diff --git a/.circleci/config.yml b/.circleci/config.yml index e14cb3a103e..e8026f915ec 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -116,13 +116,14 @@ jobs: key: ory-hydra-go-mod-v1 - run: ./test/e2e/circle-ci.bash memory - - run: ./test/e2e/circle-ci.bash memory-jwt + - run: ./test/e2e/circle-ci.bash memory --jwt - run: ./test/e2e/circle-ci.bash cockroach - - run: ./test/e2e/circle-ci.bash cockroach-jwt + - run: ./test/e2e/circle-ci.bash cockroach --jwt - run: ./test/e2e/circle-ci.bash mysql - - run: ./test/e2e/circle-ci.bash mysql-jwt + - run: ./test/e2e/circle-ci.bash mysql --jwt - run: ./test/e2e/circle-ci.bash postgres - - run: ./test/e2e/circle-ci.bash postgres-jwt + - run: ./test/e2e/circle-ci.bash postgres --jwt + workflows: bdt: diff --git a/.docker/Dockerfile-alpine b/.docker/Dockerfile-alpine index 1efe9e82592..ee1f2b785f2 100644 --- a/.docker/Dockerfile-alpine +++ b/.docker/Dockerfile-alpine @@ -1,4 +1,4 @@ -FROM alpine:3.14.3 +FROM alpine:3.15 RUN addgroup -S ory; \ adduser -S ory -G ory -D -H -s /bin/nologin diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build index 3567f860e06..3a04b737b62 100644 --- a/.docker/Dockerfile-build +++ b/.docker/Dockerfile-build @@ -1,4 +1,4 @@ -FROM golang:1.16-alpine AS builder +FROM golang:1.17-alpine3.15 AS builder RUN apk -U --no-cache add build-base git gcc bash @@ -16,7 +16,7 @@ ADD . . RUN go build -tags sqlite -o /usr/bin/hydra -FROM alpine:3.14.3 +FROM alpine:3.15 RUN addgroup -S ory; \ adduser -S ory -G ory -D -h /home/ory -s /bin/nologin; \ diff --git a/.docker/Dockerfile-scratch b/.docker/Dockerfile-scratch index 12fb2ac3df0..dafd2f60c53 100644 --- a/.docker/Dockerfile-scratch +++ b/.docker/Dockerfile-scratch @@ -1,4 +1,4 @@ -FROM alpine:3.14.3 +FROM alpine:3.15 RUN apk add -U --no-cache ca-certificates diff --git a/.docker/Dockerfile-sqlite b/.docker/Dockerfile-sqlite index a8717de2037..f31b4e21c53 100644 --- a/.docker/Dockerfile-sqlite +++ b/.docker/Dockerfile-sqlite @@ -1,4 +1,4 @@ -FROM alpine:3.14.3 +FROM alpine:3.15 # Because this image is built for SQLite, we create /home/ory and /home/ory/sqlite which is owned by the ory user # and declare /home/ory/sqlite a volume. diff --git a/Makefile b/Makefile index a94f2d3b3e8..406a8b26e19 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ test-resetdb: node_modules docker rm -f hydra_test_database_mysql || true docker rm -f hydra_test_database_postgres || true docker rm -f hydra_test_database_cockroach || true - docker run --rm --name hydra_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7 + docker run --rm --name hydra_test_database_mysql --platform linux/amd64 -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7 docker run --rm --name hydra_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:9.6 docker run --rm --name hydra_test_database_cockroach -p 3446:26257 -d cockroachdb/cockroach:v20.2.6 start-single-node --insecure @@ -71,14 +71,10 @@ docker: .PHONY: e2e e2e: node_modules test-resetdb source ./scripts/test-env.sh - ./test/e2e/circle-ci.bash memory - ./test/e2e/circle-ci.bash memory-jwt - ./test/e2e/circle-ci.bash postgres - ./test/e2e/circle-ci.bash postgres-jwt - ./test/e2e/circle-ci.bash mysql - ./test/e2e/circle-ci.bash mysql-jwt - ./test/e2e/circle-ci.bash cockroach - ./test/e2e/circle-ci.bash cockroach-jwt + for db in memory postgres mysql cockroach; do \ + ./test/e2e/circle-ci.bash "$${db}"; \ + ./test/e2e/circle-ci.bash "$${db}" --jwt; \ + done # Runs tests in short mode, without database adapters .PHONY: quicktest diff --git a/README.md b/README.md index 090a6e90fb6..335c7a6c140 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ that your company deserves a spot here, reach out to DataDetect Datadetect unifiedglobalarchiving.com/data-detect/ - + Adopter * Sainsbury's @@ -190,7 +190,7 @@ that your company deserves a spot here, reach out to Reyah Reyah reyah.eu - + Adopter * Zero @@ -264,26 +264,6 @@ TheCrealm. \* Uses one of Ory's major projects in production. - - - - - - - - - - - - - - - - - - - - ### OAuth2 and OpenID Connect: Open Standards! @@ -295,6 +275,7 @@ ORY Hydra implements Open Standards set by the IETF: * [OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) * [OAuth 2.0 for Native Apps](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-10) * [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636) +* [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523) and the OpenID Foundation: @@ -543,7 +524,7 @@ you are trying to fix something very specific and need the database tests all th suggest that you initialize the databases with: ```shell script -make resetdb +make test-resetdb export TEST_DATABASE_MYSQL='mysql://root:secret@(127.0.0.1:3444)/mysql?parseTime=true&multiStatements=true' export TEST_DATABASE_POSTGRESQL='postgres://postgres:secret@127.0.0.1:3445/postgres?sslmode=disable' export TEST_DATABASE_COCKROACHDB='cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable' @@ -579,7 +560,7 @@ type of tests very difficult, but thankfully you can run the e2e test in the bro or if you would like to test one of the databases: ```shell script -make resetdb +make test-resetdb export TEST_DATABASE_MYSQL='mysql://root:secret@(127.0.0.1:3444)/mysql?parseTime=true&multiStatements=true' export TEST_DATABASE_POSTGRESQL='postgres://postgres:secret@127.0.0.1:3445/postgres?sslmode=disable' export TEST_DATABASE_COCKROACHDB='cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable' diff --git a/cmd/cli/handler_janitor.go b/cmd/cli/handler_janitor.go index 944c28d29ff..62afb038d36 100644 --- a/cmd/cli/handler_janitor.go +++ b/cmd/cli/handler_janitor.go @@ -28,6 +28,7 @@ const ( ConsentRequestLifespan = "consent-request-lifespan" OnlyTokens = "tokens" OnlyRequests = "requests" + OnlyGrants = "grants" ReadFromEnv = "read-from-env" Config = "config" ) @@ -50,9 +51,9 @@ func (_ *JanitorHandler) Args(cmd *cobra.Command, args []string) error { "- Using the config file with flag -c, --config") } - if !flagx.MustGetBool(cmd, OnlyTokens) && !flagx.MustGetBool(cmd, OnlyRequests) { + if !flagx.MustGetBool(cmd, OnlyTokens) && !flagx.MustGetBool(cmd, OnlyRequests) && !flagx.MustGetBool(cmd, OnlyGrants) { return fmt.Errorf("%s\n%s\n", cmd.UsageString(), - "Janitor requires either --tokens or --requests or both to be set") + "Janitor requires at least one of --tokens, --requests or --grants to be set") } limit := flagx.MustGetInt(cmd, Limit) @@ -137,6 +138,10 @@ func purge(cmd *cobra.Command, args []string) error { routineFlags = append(routineFlags, OnlyRequests) } + if flagx.MustGetBool(cmd, OnlyGrants) { + routineFlags = append(routineFlags, OnlyGrants) + } + return cleanupRun(cmd.Context(), notAfter, limit, batchSize, addRoutine(p, routineFlags...)...) } @@ -149,6 +154,8 @@ func addRoutine(p persistence.Persister, names ...string) []cleanupRoutine { routines = append(routines, cleanup(p.FlushInactiveRefreshTokens, "refresh tokens")) case OnlyRequests: routines = append(routines, cleanup(p.FlushInactiveLoginConsentRequests, "login-consent requests")) + case OnlyGrants: + routines = append(routines, cleanup(p.FlushInactiveGrants, "grants")) } } return routines diff --git a/cmd/cli/handler_janitor_test.go b/cmd/cli/handler_janitor_test.go index 52c45149192..ae1f95701b3 100644 --- a/cmd/cli/handler_janitor_test.go +++ b/cmd/cli/handler_janitor_test.go @@ -203,12 +203,17 @@ func TestJanitorHandler_Arguments(t *testing.T) { fmt.Sprintf("--%s", cli.OnlyTokens), "memory", ) + cmdx.ExecNoErr(t, cmd.NewRootCmd(), + "janitor", + fmt.Sprintf("--%s", cli.OnlyGrants), + "memory", + ) _, _, err := cmdx.ExecCtx(context.Background(), cmd.NewRootCmd(), nil, "janitor", "memory") require.Error(t, err) - require.Contains(t, err.Error(), "Janitor requires either --tokens or --requests or both to be set") + require.Contains(t, err.Error(), "Janitor requires at least one of --tokens, --requests or --grants to be set") cmdx.ExecNoErr(t, cmd.NewRootCmd(), "janitor", @@ -259,3 +264,35 @@ func TestJanitorHandler_Arguments(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "Value for --batch-size must not be greater than value for --limit") } + +func TestJanitorHandler_PurgeGrantNotAfter(t *testing.T) { + ctx := context.Background() + testCycles := testhelpers.NewConsentJanitorTestHelper("").GetNotAfterTestCycles() + + require.True(t, len(testCycles) > 0) + + for k, v := range testCycles { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + jt := testhelpers.NewConsentJanitorTestHelper(t.Name()) + reg, err := jt.GetRegistry(ctx, k) + require.NoError(t, err) + + // setup test + t.Run("step=setup", jt.GrantNotAfterSetup(ctx, reg.ClientManager(), reg.GrantManager())) + + // run the cleanup routine + t.Run("step=cleanup", func(t *testing.T) { + cmdx.ExecNoErr(t, newJanitorCmd(), + "janitor", + fmt.Sprintf("--%s=%s", cli.KeepIfYounger, v.String()), + fmt.Sprintf("--%s", cli.OnlyGrants), + jt.GetDSN(), + ) + }) + + // validate test + notAfter := time.Now().Round(time.Second).Add(-v) + t.Run("step=validate-access", jt.GrantNotAfterValidate(ctx, notAfter, reg.GrantManager())) + }) + } +} diff --git a/cmd/janitor.go b/cmd/janitor.go index a25a9a40930..6b7105cf6cd 100644 --- a/cmd/janitor.go +++ b/cmd/janitor.go @@ -10,7 +10,7 @@ import ( func NewJanitorCmd() *cobra.Command { cmd := &cobra.Command{ Use: "janitor []", - Short: "Clean the database of old tokens and login/consent requests", + Short: "Clean the database of old tokens, login/consent requests and jwt grant issuers", Long: `This command will cleanup any expired oauth2 tokens as well as login/consent requests. This will select records to delete with a limit and delete records in batch to ensure that no table locking issues arise in big production databases. @@ -46,9 +46,13 @@ Janitor can be used in several ways. janitor --requests - or both + or - janitor --tokens --requests + janitor --grants + + or any combination of them + + janitor --tokens --requests --grants `, RunE: cli.NewHandler().Janitor.RunE, Args: cli.NewHandler().Janitor.Args, @@ -59,8 +63,9 @@ Janitor can be used in several ways. cmd.Flags().Duration(cli.AccessLifespan, 0, "Set the access token lifespan e.g. 1s, 1m, 1h.") cmd.Flags().Duration(cli.RefreshLifespan, 0, "Set the refresh token lifespan e.g. 1s, 1m, 1h.") cmd.Flags().Duration(cli.ConsentRequestLifespan, 0, "Set the login/consent request lifespan e.g. 1s, 1m, 1h") - cmd.Flags().Bool(cli.OnlyRequests, false, "This will only run the cleanup on requests and will skip token cleanup.") - cmd.Flags().Bool(cli.OnlyTokens, false, "This will only run the cleanup on tokens and will skip requests cleanup.") + cmd.Flags().Bool(cli.OnlyRequests, false, "This will only run the cleanup on requests and will skip token and trust relationships cleanup.") + cmd.Flags().Bool(cli.OnlyTokens, false, "This will only run the cleanup on tokens and will skip requests and trust relationships cleanup.") + cmd.Flags().Bool(cli.OnlyGrants, false, "This will only run the cleanup on trust relationships and will skip requests and token cleanup.") cmd.Flags().BoolP(cli.ReadFromEnv, "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") configx.RegisterFlags(cmd.PersistentFlags()) return cmd diff --git a/cypress/helpers/index.js b/cypress/helpers/index.js index 9e0730a400a..c57d12cb307 100644 --- a/cypress/helpers/index.js +++ b/cypress/helpers/index.js @@ -1,7 +1,9 @@ -export const prng = () => - `${Math.random().toString(36).substring(2)}${Math.random() - .toString(36) - .substring(2)}` +export const prng = () => { + var array = new Uint32Array(2) + crypto.getRandomValues(array) + + return `${array[0].toString()}${array[1].toString()}` +} const isStatusOk = (res) => res.ok @@ -56,3 +58,43 @@ const getClient = (id) => cy .request(Cypress.env('admin_url') + '/clients/' + id) .then(({ body }) => body) + +export const createGrant = (grant) => + cy + .request( + 'POST', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers', + JSON.stringify(grant) + ) + .then((response) => { + const grantID = response.body.id + getGrant(grantID).then((actual) => { + if (actual.id !== grantID) { + return Promise.reject( + new Error(`Expected id's to match: ${actual.id} !== ${grantID}`) + ) + } + return Promise.resolve(response) + }) + }) + +export const getGrant = (grantID) => + cy + .request( + 'GET', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers/' + grantID + ) + .then(({ body }) => body) + +export const deleteGrants = () => + cy + .request(Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers') + .then(({ body = [] }) => { + ;(body || []).forEach(({ id }) => deleteGrant(id)) + }) + +const deleteGrant = (id) => + cy.request( + 'DELETE', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers/' + id + ) diff --git a/cypress/integration/admin/grant_jwtbearer.js b/cypress/integration/admin/grant_jwtbearer.js new file mode 100644 index 00000000000..8c3e380a747 --- /dev/null +++ b/cypress/integration/admin/grant_jwtbearer.js @@ -0,0 +1,123 @@ +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) +dayjs.extend(isBetween) + +describe('The JWT-Bearer Grants Admin Interface', () => { + let d = dayjs().utc().add(1, 'year').set('millisecond', 0) + const newGrant = (issuer = 'token-service', subject = 'bob@example.com') => ({ + issuer, + subject, + expires_at: d.toISOString(), + scope: ['openid', 'offline'], + jwk: { + use: 'sig', + kty: 'RSA', + kid: 'token-service-key', + alg: 'RS256', + n: + 'ue1_WT_RU6Lc65dmmD7llh9Tcu_Xc909be1Yr5xlHUpkVzacHhSgjliSjUnGCuMo1-m3ILktgt3p86ba6bmIk9fK3nKA7OztDymHuuaYGbJVHhDSKcCBMXGFPcBLxtEns7nvMoQ-lkFN-kYgfSfg0iPGXeRo2Io7phqr54pBaEG_xMK9c-rQ_G3Y9eXn1JREEgQd4OvA2UR9Vc4E-xAYMx7V-ZOvMeKBj9HACE8cllnpKlEKLMo5O5BvkpqA1MeOtzL5jxUUH8D37TJvVQ67VgTs40dRwWwRePfIMDHRJSeJ0KTpkgnX4fmaF2xfi53N8hM9PHzzCtaWrjzm1r1Gyw', + e: 'AQAB' + } + }) + + beforeEach(() => { + // Clean up all previous grants + cy.request( + 'GET', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers' + ).then((response) => { + response.body.map(({ id }) => { + cy.request( + 'delete', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers/' + id + ).then(() => {}) + }) + }) + }) + + it('should return newly created jwt-bearer grant and grant can be retrieved later', () => { + const grant = newGrant() + const start = dayjs().subtract(1, 'minutes') + const end = dayjs().add(1, 'minutes') + cy.request( + 'POST', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers', + JSON.stringify(grant) + ).then((response) => { + const createdAt = dayjs(response.body.created_at) + const expiresAt = dayjs(response.body.expires_at) + const grantID = response.body.id + + expect(response.body.issuer).to.equal(grant.issuer) + expect(response.body.subject).to.equal(grant.subject) + expect(createdAt.isBetween(start, end)).to.true + expect(expiresAt.isSame(grant.expires_at)).to.true + expect(response.body.scope).to.deep.equal(grant.scope) + expect(response.body.public_key.set).to.equal(grant.issuer) + expect(response.body.public_key.kid).to.equal(grant.jwk.kid) + + cy.request( + 'GET', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers/' + grantID + ).then((response) => { + expect(response.body.issuer).to.equal(grant.issuer) + expect(response.body.subject).to.equal(grant.subject) + expect(response.body.scope).to.deep.equal(grant.scope) + expect(response.body.public_key.set).to.equal(grant.issuer) + expect(response.body.public_key.kid).to.equal(grant.jwk.kid) + }) + }) + }) + + it('should return newly created jwt-bearer grant in grants list', () => { + // We have exactly one grant + const grant = newGrant() + cy.request( + 'POST', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers', + JSON.stringify(grant) + ).then(() => {}) + cy.request( + 'GET', + Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers' + ).then((response) => { + expect(response.body).to.length(1) + }) + }) + + it('should fail, because the same grant is already exist', () => { + const grant = newGrant() + cy.request({ + method: 'POST', + url: Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers', + failOnStatusCode: false, + body: JSON.stringify(grant) + }).then((response) => { + expect(response.status).to.equal(201) + }) + + cy.request({ + method: 'POST', + url: Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers', + failOnStatusCode: false, + body: JSON.stringify(grant) + }).then((response) => { + expect(response.status).to.equal(409) + }) + }) + + it('should fail, because trying to create grant with no issuer', () => { + const grant = newGrant() + grant.issuer = '' + cy.request({ + method: 'POST', + url: Cypress.env('admin_url') + '/trust/grants/jwt-bearer/issuers', + failOnStatusCode: false, + body: JSON.stringify(grant) + }).then((response) => { + expect(response.status).to.equal(400) + }) + }) +}) diff --git a/cypress/integration/oauth2/grant_jwtbearer.js b/cypress/integration/oauth2/grant_jwtbearer.js new file mode 100644 index 00000000000..5007efc13cd --- /dev/null +++ b/cypress/integration/oauth2/grant_jwtbearer.js @@ -0,0 +1,516 @@ +import { + createClient, + createGrant, + deleteGrants, + deleteClients, + 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 +} + +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_id: prng(), + 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'] + }) + + const gr = (subject) => ({ + issuer: prng(), + 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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 Error (400) when given client credentials and a JWT assertion with an invalid issuer', function () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 () { + const client = nc() + createClient(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 + }) + }) +}) diff --git a/docs/docs/advanced.md b/docs/docs/advanced.md index 48d62211ff7..3a22f6a04e8 100644 --- a/docs/docs/advanced.md +++ b/docs/docs/advanced.md @@ -239,148 +239,8 @@ Hydra will gracefully deny refresh requests if the hook responds with ### OAuth 2.0 Client Authentication with private/public keypairs -ORY Hydra supports OAuth 2.0 Client Authentication with RSA and ECDSA -private/public keypairs with currently supported signing algorithms: - -- RS256 (default), RS384, RS512 -- PS256, PS384, PS512 -- ES256, ES384, ES512 -- EdDSA - -This authentication method replaces the classic HTTP Basic Authorization and -HTTP POST Authorization schemes. Instead of sending the `client_id` and -`client_secret`, you authenticate the client with a signed JSON Web Token. - -To enable this feature for a specific OAuth 2.0 Client, you must set -`token_endpoint_auth_method` to `private_key_jwt` and register the public key of -the RSA/ECDSA signing key either using the `jwks_uri` or `jwks` fields of the -client. - -When authenticating the client at the token endpoint, you generate and sign -(with the RSA/ECDSA private key) a JSON Web Token with the following claims: - -- `iss`: REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client. -- `sub`: REQUIRED. Subject. This MUST contain the client_id of the OAuth Client. -- `aud`: REQUIRED. Audience. The aud (audience) Claim. Value that identifies the - Authorization Server (ORY Hydra) as an intended audience. The Authorization - Server MUST verify that it is an intended audience for the token. The Audience - SHOULD be the URL of the Authorization Server's Token Endpoint. -- `jti`: REQUIRED. JWT ID. A unique identifier for the token, which can be used - to prevent reuse of the token. These tokens MUST only be used once, unless - conditions for reuse were negotiated between the parties; any such negotiation - is beyond the scope of this specification. -- `exp`: REQUIRED. Expiration time on or after which the ID Token MUST NOT be - accepted for processing. -- `iat`: OPTIONAL. Time at which the JWT was issued. - -When making a request to the `/oauth2/token` endpoint, you include -`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` -and `client_assertion=` in the request body: - -``` -POST /oauth2/token HTTP/1.1 -Host: my-hydra.com -Content-Type: application/x-www-form-urlencoded - -grant_type=authorization_code& -code=i1WsRn1uB1& -client_id=s6BhdRkqt3& -client_assertion_type= -urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer& -client_assertion=PHNhbWxwOl ... ZT -``` - -Here's what a client with a `jwks` containing one RSA public key looks like: - -```json -{ - "client_id": "rsa-client-jwks", - "jwks": { - "keys": [ - { - "kty": "RSA", - "n": "jL7h5wc-yeMUsHGJHc0xe9SbTdaLKXMHvcIHQck20Ji7SvrHPdTDQTvZtTDS_wJYbeShcCrliHvbJRSZhtEe0mPJpyWg3O_HkKy6_SyHepLK-_BR7HfcXYB6pVJCG3BW-lVMY7gl5sULFA74kNZH50h8hdmyWC9JgOHn0n3YLdaxSWlhctuwNPSwqwzY4qtN7_CZub81SXWpKiwj4UpyB10b8rM8qn35FS1hfsaFCVi0gQpd4vFDgFyqqpmiwq8oMr8RZ2mf0NMKCP3RXnMhy9Yq8O7lgG2t6g1g9noWbzZDUZNc54tv4WGFJ_rJZRz0jE_GR6v5sdqsDTdjFquPlQ", - "e": "AQAB", - "use": "sig", - "kid": "rsa-jwk" - } - ] - }, - "token_endpoint_auth_method": "private_key_jwt", - "token_endpoint_auth_signing_alg": "RS256" -} -``` - -And here is how it looks like for a `jwks` including an ECDSA public key: - -```json -{ - "client_id": "ecdsa-client-jwks", - "jwks": { - "keys": [ - { - "kty": "EC", - "use": "sig", - "crv": "P-256", - "kid": "ecdsa-jwk", - "x": "nQjdhpecjZRlworpYk_TJAQBe4QbS8IwHY1DWkfR0w0", - "y": "UQfLzHxhc4i3EETUeaAS1vDVFJ-Y01hIESiXqqS86Vc" - } - ] - }, - "token_endpoint_auth_method": "private_key_jwt", - "token_endpoint_auth_signing_alg": "ES256" -} -``` - -And here is how it looks like for a `jwks` including an EdDSA public key: - -```json -{ - "client_id": "eddsa-client-jwks", - "jwks": { - "keys": [ - { - "kty": "OKP", - "use": "sig", - "crv": "Ed25519", - "kid": "eddsa-jwk", - "x": "cg1qGqQGSF6xvzoDZVaDfxu0c2fPhUEuVHYUr1WYVXs" - } - ] - }, - "token_endpoint_auth_method": "private_key_jwt", - "token_endpoint_auth_signing_alg": "EdDSA" -} -``` - -And with `jwks_uri`: - -```json -{ - "client_id": "client-jwks-uri", - "jwks_uri": "http://path-to-my-public/keys.json", - "token_endpoint_auth_method": "private_key_jwt", - "token_endpoint_auth_signing_alg": "RS256" -} -``` - -The `jwks_uri` must return a JSON object containing the public keys associated -with the OAuth 2.0 Client: - -```json -{ - "keys": [ - { - "kty": "RSA", - "n": "jL7h5wc-yeMUsHGJHc0xe9SbTdaLKXMHvcIHQck20Ji7SvrHPdTDQTvZtTDS_wJYbeShcCrliHvbJRSZhtEe0mPJpyWg3O_HkKy6_SyHepLK-_BR7HfcXYB6pVJCG3BW-lVMY7gl5sULFA74kNZH50h8hdmyWC9JgOHn0n3YLdaxSWlhctuwNPSwqwzY4qtN7_CZub81SXWpKiwj4UpyB10b8rM8qn35FS1hfsaFCVi0gQpd4vFDgFyqqpmiwq8oMr8RZ2mf0NMKCP3RXnMhy9Yq8O7lgG2t6g1g9noWbzZDUZNc54tv4WGFJ_rJZRz0jE_GR6v5sdqsDTdjFquPlQ", - "e": "AQAB", - "use": "sig", - "kid": "rsa-jwk" - } - ] -} -``` +Please head over to the +[RFC7523 Documentation](guides/oauth2-grant-type-jwt-bearer.mdx). ## OpenID Connect diff --git a/docs/docs/concepts/consent.mdx b/docs/docs/concepts/consent.mdx index 225ed42b82f..b6d378f115b 100644 --- a/docs/docs/concepts/consent.mdx +++ b/docs/docs/concepts/consent.mdx @@ -151,13 +151,11 @@ request! For more details about the implementation check the ]}> - ![Exemplary OAuth 2.0 Consent Screen](../images/consent-endpoint.png) - ```shell script $ curl \ "http://127.0.0.1:4445/oauth2/auth/requests/consent?consent_challenge=7bb518c4eec2454dbb289f5fdb4c0ee2" @@ -169,7 +167,6 @@ examples using the ORY Hydra SDK in different languages. - ```json { "challenge": "f633e49d56bc40e0a876ac8242eb9891", @@ -216,7 +213,6 @@ examples using the ORY Hydra SDK in different languages. - The way you collect the consent information from the End-User is up to you. In most cases, you will show an HTML form similar to: diff --git a/docs/docs/concepts/login.mdx b/docs/docs/concepts/login.mdx index 4607d035d91..0abbc420328 100644 --- a/docs/docs/concepts/login.mdx +++ b/docs/docs/concepts/login.mdx @@ -78,13 +78,11 @@ correct endpoint for your interactions. ]}> - ![OAuth 2.0 Client](../images/oauth2-consumer.png) - ```html - ```js // ... window.location.href = @@ -106,7 +103,6 @@ window.location.href = - ## Redirection to the Login Endpoint The next task for ORY Hydra is to know the user of the request. To achieve that, @@ -206,13 +202,11 @@ more details about the implementation check the ]}> - ![OAuth 2.0 Login UI Screen](../images/login-endpoint.png) - ``` curl "http://127.0.0.1:4445/oauth2/auth/requests/login?login_challenge=7bb518c4eec2454dbb289f5fdb4c0ee2" ``` @@ -223,7 +217,6 @@ examples using the ORY Hydra SDK in different languages. - ```json { "challenge": "7bb518c4eec2454dbb289f5fdb4c0ee2", @@ -262,7 +255,6 @@ examples using the ORY Hydra SDK in different languages. - The way you authenticate the End-User is up to you. In most cases, you will show an HTML form similar to: diff --git a/docs/docs/guides/consent.mdx b/docs/docs/guides/consent.mdx index 14ba5a6a625..0fcccfee1d5 100644 --- a/docs/docs/guides/consent.mdx +++ b/docs/docs/guides/consent.mdx @@ -43,7 +43,6 @@ access to ORY Hydra's Admin Endpoint! ]}> - ![OAuth2 Consent UI Screen](../images/consent-endpoint.png) @@ -59,7 +58,6 @@ access to ORY Hydra's Admin Endpoint! - ## Accepting the Consent Request diff --git a/docs/docs/guides/login.mdx b/docs/docs/guides/login.mdx index 079804e8405..c065e394d7f 100644 --- a/docs/docs/guides/login.mdx +++ b/docs/docs/guides/login.mdx @@ -37,13 +37,11 @@ access to ORY Hydra's Admin Endpoint! ]}> - ![OAuth2 Login UI Screen](../images/login-endpoint.png) - :::note Check out our @@ -104,7 +102,6 @@ router.get('/login', csrfProtection, (req, res, next) => { - ```html
@@ -122,7 +119,6 @@ router.get('/login', csrfProtection, (req, res, next) => { - ## Accepting the Login Request { ]}> - :::note Check out our @@ -191,7 +186,6 @@ router.post('/login', csrfProtection, (req, res, next) => { - ## Rejecting the Login Request { ]}> - ```typescript // You can deny the login request at any point - for example if the system is currently undergoing maintenance // or the user has been banned, is not allowed to use OAuth2 flows, and so on: @@ -218,4 +211,3 @@ hydraAdmin - diff --git a/docs/docs/guides/logout.mdx b/docs/docs/guides/logout.mdx index b2b3f063586..02663ab1af1 100644 --- a/docs/docs/guides/logout.mdx +++ b/docs/docs/guides/logout.mdx @@ -37,13 +37,11 @@ access to ORY Hydra's Admin Endpoint! ]}> - ![OAuth2 Logout UI Screen](../images/logout-endpoint.png) - :::note Check out our @@ -96,7 +94,6 @@ router.get('/', csrfProtection, (req, res, next) => { - ```html @@ -109,7 +106,6 @@ router.get('/', csrfProtection, (req, res, next) => { - ## Accepting Logout { ]}> - :::note Check out our @@ -146,7 +141,6 @@ router.post('/logout', csrfProtection, (req, res, next) => { - ## Rejecting Logout { ]}> - :::note Check out our @@ -183,4 +176,3 @@ router.post('/logout', csrfProtection, (req, res, next) => { - diff --git a/docs/docs/guides/oauth2-grant-type-jwt-bearer.mdx b/docs/docs/guides/oauth2-grant-type-jwt-bearer.mdx new file mode 100644 index 00000000000..4f88a2fe466 --- /dev/null +++ b/docs/docs/guides/oauth2-grant-type-jwt-bearer.mdx @@ -0,0 +1,313 @@ +--- +id: oauth2-grant-type-jwt-bearer +title: JSON Web Token (JWT) Profile (RFC7523) +--- + +Ory Hydra is capable of performing the +[JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523). +This guide defines how a JWT Bearer Token can be used to request an access token +when a client wishes to utilize an existing trust relationship, expressed +through the semantics of the JWT, without a direct user-approval step at the +authorization server (Hydra). + +Ory Hydra supports both methods expressed in RFC 7523: + +- _Using JWTs as Authorization Grants_: Allows exchanging a JSON Web Token for + an Access Token. +- _Using JWTs for Client Authentication_: Allows OAuth 2.0 Client Authentication + using public/private keys via JSON Web Tokens. + +## Exchanging JWTs for Access Tokens + +To use the Authorization Grant `urn:ietf:params:oauth:grant-type:jwt-bearer`, +the client performs an OAuth 2.0 Access Token Request as defined in +[Section 4.1 of the OAuth Assertion Framework RFC7521](https://datatracker.ietf.org/doc/html/rfc7521#section-4.1) +with the following specific parameter values and encodings: + +- The value of the `grant_type` is + `urn:ietf:params:oauth:grant-type:jwt-bearer`. +- The value of the `assertion` parameter MUST contain a single JWT. + +The `scope` parameter may be used, as defined in the OAuth Assertion Framework +[RFC7521](https://datatracker.ietf.org/doc/html/rfc7521), to indicate the +requested scope: + +``` +POST /oauth2/token HTTP/1.1 +Host: public.hydra.com +Content-Type: application/x-www-form-urlencoded + +grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer +&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0. +eyJpc3Mi[...omitted for brevity...]. +J9l-ZhwP[...omitted for brevity...] +``` + +Clients using this grant must be authenticated. + +### Establishing a Trust Relationship + +Before using this grant type, you must establish a trust relationship in Ory +Hydra. This involves registering the issuer, subject, and the public key at Ory +Hydra's Admin Endpoint: + +``` +POST https:///trust/grants/jwt-bearer/issuers +Content-Type: application/json + +{ + // The issuer you want to trust. + "issuer": "https://my-issuer.com", + + // The "sub" field of the access token to be created. + "subject": "alice@example.org", + + // The allowed scope of the generated access token. + "scope": ["read"], + + // The public key with which the JWT Bearer's signature can be verified. + "jwk": { + "kty":"RSA", + "e":"AQAB", + "kid":"d8e91f55-67e0-4e56-a066-6a5f0c2efdf7", + "n":"nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw" + } + + // When this trust relationship expires. + "expires_at": "2021-04-23T18:25:43.511Z", +} +``` + +The above example would allow the following JWT Bearer + +``` +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL215LWlzc3Vlci5jb20iLCJzdWIiOiJhbGljZUBleGFtcGxlLm9yZyIsImF1ZCI6Imh0dHBzOi8vcHVibGljLmh5ZHJhLmNvbS9vYXV0aDIvdG9rZW4iLCJuYmYiOjEzMDA4MTU3ODAsImV4cCI6MTMwMDgxOTM4MH0.baBiLrVRRU1AKYvAn0X4eIeLvFfpe2wsoD3VTMtaYdbEW2-w-SFeGeEzl5B6sh612bfKkoeihhFVx2md7DP-Rl5asicJzeIhcPETzZbVSPxR1lFdOBwcIPG5N70aSJs2zSn3jnRIhpZf85YZOI8RbQ93Kxla741_4xruHbsNRFqIuWVhxk95BCCnoXzEd8vBTxd_GMn9VijUY_piLPMo-OifRF9pSjYo38aJmRW1tJzeFCMruc9X1W-2c-L_t3rV7zYBH3LlpDZfwyy3T5Pmqf6QKeq1N-MjLnIJcZGT89jqxLmqVFRvAiEyA6iMQXVxmENOnwylGPwuR8DewhWMqg +``` + +which has the claims + +```json5 +{ + iss: 'https://my-issuer.com', + sub: 'alice@example.org', + aud: 'https://public.hydra.com/oauth2/token', + nbf: 1300815780, + exp: 1300819380 +} +``` + +to be exchanged for an OAuth2 Access Token (the `scope` parameter is optional!) + +``` +POST /oauth2/token HTTP/1.1 +Host: public.hydra.com +Content-Type: application/x-www-form-urlencoded + +grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer +&scope=read +&assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL215LWlzc3Vlci5jb20iLCJzdWIiOiJhbGljZUBleGFtcGxlLm9yZyIsImF1ZCI6Imh0dHBzOi8vcHVibGljLmh5ZHJhLmNvbS9vYXV0aDIvdG9rZW4iLCJuYmYiOjEzMDA4MTU3ODAsImV4cCI6MTMwMDgxOTM4MH0.baBiLrVRRU1AKYvAn0X4eIeLvFfpe2wsoD3VTMtaYdbEW2-w-SFeGeEzl5B6sh612bfKkoeihhFVx2md7DP-Rl5asicJzeIhcPETzZbVSPxR1lFdOBwcIPG5N70aSJs2zSn3jnRIhpZf85YZOI8RbQ93Kxla741_4xruHbsNRFqIuWVhxk95BCCnoXzEd8vBTxd_GMn9VijUY_piLPMo-OifRF9pSjYo38aJmRW1tJzeFCMruc9X1W-2c-L_t3rV7zYBH3LlpDZfwyy3T5Pmqf6QKeq1N-MjLnIJcZGT89jqxLmqVFRvAiEyA6iMQXVxmENOnwylGPwuR8DewhWMqg +``` + +with resulting access token claims: + +``` +{ + "iss": "https://public.hydra.com/", + "sub": "alice@example.org", + "scp": ["read"], + // ... +} +``` + +You can also delete, get, and list trust relationships. Please check the +[HTTP REST API documentation](../reference/api.mdx) for more details. + +### OAuth2 JWT Bearer Grant Type Validation + +When performing the `urn:ietf:params:oauth:grant-type:jwt-bearer` Authorization +Grant, the JWT Bearer in the `assertion` parameter is validated as follows: + +1. The JWT MUST contain an `iss` (issuer) claim that contains a unique + identifier for the entity that issued the JWT. The value must match the + `issuer` value of the trust relationship. +2. The JWT MUST contain a `sub` (subject) claim identifying the principal that + is the subject of the JWT (e.g. user ID). The value must match the `subject` + value of the trust relationship. +3. The JWT MUST contain an `aud` (audience) claim containing a value that + identifies the authorization server (Hydra) as an intended audience. So this + value must be Hydra Token URL. +4. The JWT MUST contain an `exp` (expiration time) claim that limits the time + window during which the JWT can be used. Can be controlled by + `oauth2.grant.jwt.max_ttl` setting. +5. The JWT MAY contain an `nbf` (not before) claim that identifies the time + before which the token MUST NOT be accepted for processing by Hydra. +6. The JWT MAY contain an `iat` (issued at) claim that identifies the time at + which the JWT was issued. Controlled by `oauth2.grant.jwt.iat_optional` + (default `false`) If `iat` is not passed, then current time (when assertion + is received by Hydra) will be considered as issued date. +7. The JWT MAY contain a `jti` (JWT ID) claim that provides a unique identifier + for the token. Controlled by `oauth2.grant.jwt.jti_optional` (default + `false`) setting. **Note**: If `jti` is configured to be required, then Hydra + will reject all assertions with the same `jti`, if `jti` was already used by + some assertion, and this assertion is not expired yet (see `exp` claim). +8. The JWT MUST be digitally signed. + +If a scope was included in the OAuth2 Access Token Request + +``` +POST /oauth2/token HTTP/1.1 +Host: public.hydra.com +Content-Type: application/x-www-form-urlencoded + +grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer +&scope=read +&assertion=... +``` + +Hydra will check them against scopes defined in the corresponding trust +relationship. + +## Using JWTs for Client Authentication + +ORY Hydra supports OAuth 2.0 Client Authentication with RSA and ECDSA +private/public keypairs with currently supported signing algorithms: + +- RS256 (default), RS384, RS512 +- PS256, PS384, PS512 +- ES256, ES384, ES512 +- EdDSA + +This authentication method replaces the classic HTTP Basic Authorization and +HTTP POST Authorization schemes. Instead of sending the `client_id` and +`client_secret`, you authenticate the client with a signed JSON Web Token. + +To enable this feature for a specific OAuth 2.0 Client, you must set +`token_endpoint_auth_method` to `private_key_jwt` and register the public key of +the RSA/ECDSA signing key either using the `jwks_uri` or `jwks` fields of the +client. + +When authenticating the client at the token endpoint, you generate and sign +(with the RSA/ECDSA private key) a JSON Web Token with the following claims: + +- `iss`: REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client. +- `sub`: REQUIRED. Subject. This MUST contain the client_id of the OAuth Client. +- `aud`: REQUIRED. Audience. The aud (audience) Claim. Value that identifies the + Authorization Server (ORY Hydra) as an intended audience. The Authorization + Server MUST verify that it is an intended audience for the token. The Audience + SHOULD be the URL of the Authorization Server's Token Endpoint. +- `jti`: REQUIRED. JWT ID. A unique identifier for the token, which can be used + to prevent reuse of the token. These tokens MUST only be used once, unless + conditions for reuse were negotiated between the parties; any such negotiation + is beyond the scope of this specification. +- `exp`: REQUIRED. Expiration time on or after which the ID Token MUST NOT be + accepted for processing. +- `iat`: OPTIONAL. Time at which the JWT was issued. + +When making a request to the `/oauth2/token` endpoint, you include +`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` +and `client_assertion=` in the request body: + +``` +POST /oauth2/token HTTP/1.1 +Host: my-hydra.com +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code& +code=i1WsRn1uB1& +client_id=s6BhdRkqt3& +client_assertion_type= +urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer& +client_assertion=PHNhbWxwOl ... ZT +``` + +Here's what a client with a `jwks` containing one RSA public key looks like: + +```json +{ + "client_id": "rsa-client-jwks", + "jwks": { + "keys": [ + { + "kty": "RSA", + "n": "jL7h5wc-yeMUsHGJHc0xe9SbTdaLKXMHvcIHQck20Ji7SvrHPdTDQTvZtTDS_wJYbeShcCrliHvbJRSZhtEe0mPJpyWg3O_HkKy6_SyHepLK-_BR7HfcXYB6pVJCG3BW-lVMY7gl5sULFA74kNZH50h8hdmyWC9JgOHn0n3YLdaxSWlhctuwNPSwqwzY4qtN7_CZub81SXWpKiwj4UpyB10b8rM8qn35FS1hfsaFCVi0gQpd4vFDgFyqqpmiwq8oMr8RZ2mf0NMKCP3RXnMhy9Yq8O7lgG2t6g1g9noWbzZDUZNc54tv4WGFJ_rJZRz0jE_GR6v5sdqsDTdjFquPlQ", + "e": "AQAB", + "use": "sig", + "kid": "rsa-jwk" + } + ] + }, + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "RS256" +} +``` + +And here is how it looks like for a `jwks` including an ECDSA public key: + +```json +{ + "client_id": "ecdsa-client-jwks", + "jwks": { + "keys": [ + { + "kty": "EC", + "use": "sig", + "crv": "P-256", + "kid": "ecdsa-jwk", + "x": "nQjdhpecjZRlworpYk_TJAQBe4QbS8IwHY1DWkfR0w0", + "y": "UQfLzHxhc4i3EETUeaAS1vDVFJ-Y01hIESiXqqS86Vc" + } + ] + }, + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "ES256" +} +``` + +And here is how it looks like for a `jwks` including an EdDSA public key: + +```json +{ + "client_id": "eddsa-client-jwks", + "jwks": { + "keys": [ + { + "kty": "OKP", + "use": "sig", + "crv": "Ed25519", + "kid": "eddsa-jwk", + "x": "cg1qGqQGSF6xvzoDZVaDfxu0c2fPhUEuVHYUr1WYVXs" + } + ] + }, + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "EdDSA" +} +``` + +And with `jwks_uri`: + +```json +{ + "client_id": "client-jwks-uri", + "jwks_uri": "http://path-to-my-public/keys.json", + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "RS256" +} +``` + +The `jwks_uri` must return a JSON object containing the public keys associated +with the OAuth 2.0 Client: + +```json +{ + "keys": [ + { + "kty": "RSA", + "n": "jL7h5wc-yeMUsHGJHc0xe9SbTdaLKXMHvcIHQck20Ji7SvrHPdTDQTvZtTDS_wJYbeShcCrliHvbJRSZhtEe0mPJpyWg3O_HkKy6_SyHepLK-_BR7HfcXYB6pVJCG3BW-lVMY7gl5sULFA74kNZH50h8hdmyWC9JgOHn0n3YLdaxSWlhctuwNPSwqwzY4qtN7_CZub81SXWpKiwj4UpyB10b8rM8qn35FS1hfsaFCVi0gQpd4vFDgFyqqpmiwq8oMr8RZ2mf0NMKCP3RXnMhy9Yq8O7lgG2t6g1g9noWbzZDUZNc54tv4WGFJ_rJZRz0jE_GR6v5sdqsDTdjFquPlQ", + "e": "AQAB", + "use": "sig", + "kid": "rsa-jwk" + } + ] +} +``` diff --git a/docs/scripts/config.js b/docs/scripts/config.js index 058a030e056..ce2bc434d81 100644 --- a/docs/scripts/config.js +++ b/docs/scripts/config.js @@ -45,98 +45,100 @@ if (process.argv.length !== 3 || process.argv[1] === 'help') { const config = require(path.resolve(process.argv[2])) -const enhance = (schema, parents = []) => (item) => { - const key = item.key.value - - const path = [ - ...parents.map((parent) => ['properties', parent]), - ['properties', key] - ].flat() - - if (['title', 'description'].find((f) => path[path.length - 1] === f)) { - return - } +const enhance = + (schema, parents = []) => + (item) => { + const key = item.key.value + + const path = [ + ...parents.map((parent) => ['properties', parent]), + ['properties', key] + ].flat() + + if (['title', 'description'].find((f) => path[path.length - 1] === f)) { + return + } - const comments = [`# ${pathOr(key, [...path, 'title'], schema)} ##`, ''] + const comments = [`# ${pathOr(key, [...path, 'title'], schema)} ##`, ''] - const description = pathOr('', [...path, 'description'], schema) - if (description) { - comments.push(' ' + description.split('\n').join('\n '), '') - } + const description = pathOr('', [...path, 'description'], schema) + if (description) { + comments.push(' ' + description.split('\n').join('\n '), '') + } - const defaultValue = pathOr('', [...path, 'default'], schema) - if (defaultValue || defaultValue === false) { - comments.push(' Default value: ' + defaultValue, '') - } + const defaultValue = pathOr('', [...path, 'default'], schema) + if (defaultValue || defaultValue === false) { + comments.push(' Default value: ' + defaultValue, '') + } - const enums = pathOr('', [...path, 'enum'], schema) - if (enums && Array.isArray(enums)) { - comments.push( - ' One of:', - ...YAML.stringify(enums) - .split('\n') - .map((i) => ` ${i}`) - ) // split always returns one empty object so no need for newline - } + const enums = pathOr('', [...path, 'enum'], schema) + if (enums && Array.isArray(enums)) { + comments.push( + ' One of:', + ...YAML.stringify(enums) + .split('\n') + .map((i) => ` ${i}`) + ) // split always returns one empty object so no need for newline + } - const min = pathOr('', [...path, 'minimum'], schema) - if (min || min === 0) { - comments.push(` Minimum value: ${min}`, '') - } + const min = pathOr('', [...path, 'minimum'], schema) + if (min || min === 0) { + comments.push(` Minimum value: ${min}`, '') + } - const max = pathOr('', [...path, 'maximum'], schema) - if (max || max === 0) { - comments.push(` Maximum value: ${max}`, '') - } + const max = pathOr('', [...path, 'maximum'], schema) + if (max || max === 0) { + comments.push(` Maximum value: ${max}`, '') + } - const examples = pathOr('', [...path, 'examples'], schema) - if (examples) { - comments.push( - ' Examples:', - ...YAML.stringify(examples) - .split('\n') - .map((i) => ` ${i}`) - ) // split always returns one empty object so no need for newline - } + const examples = pathOr('', [...path, 'examples'], schema) + if (examples) { + comments.push( + ' Examples:', + ...YAML.stringify(examples) + .split('\n') + .map((i) => ` ${i}`) + ) // split always returns one empty object so no need for newline + } - let hasChildren - if (item.value.items) { - item.value.items.forEach((item) => { - if (item.key) { - enhance(schema, [...parents, key])(item) - hasChildren = true - } - }) - } + let hasChildren + if (item.value.items) { + item.value.items.forEach((item) => { + if (item.key) { + enhance(schema, [...parents, key])(item) + hasChildren = true + } + }) + } - const showEnvVarBlockForObject = pathOr( - '', - [...path, 'showEnvVarBlockForObject'], - schema - ) - if (!hasChildren || showEnvVarBlockForObject) { - const env = [...parents, key].map((i) => i.toUpperCase()).join('_') - comments.push( - ' Set this value using environment variables on', - ' - Linux/macOS:', - ` $ export ${env}=`, - ' - Windows Command Line (CMD):', - ` > set ${env}=`, - '' + const showEnvVarBlockForObject = pathOr( + '', + [...path, 'showEnvVarBlockForObject'], + schema ) - - // Show this if the config property is an object, to call out how to specify the env var - if (hasChildren) { + if (!hasChildren || showEnvVarBlockForObject) { + const env = [...parents, key].map((i) => i.toUpperCase()).join('_') comments.push( - ' This can be set as an environment variable by supplying it as a JSON object.', + ' Set this value using environment variables on', + ' - Linux/macOS:', + ` $ export ${env}=`, + ' - Windows Command Line (CMD):', + ` > set ${env}=`, '' ) + + // Show this if the config property is an object, to call out how to specify the env var + if (hasChildren) { + comments.push( + ' This can be set as an environment variable by supplying it as a JSON object.', + '' + ) + } } - } - item.commentBefore = comments.join('\n') - item.spaceBefore = true -} + item.commentBefore = comments.join('\n') + item.spaceBefore = true + } new Promise((resolve, reject) => { parser.dereference( diff --git a/docs/scripts/rerelease.js b/docs/scripts/rerelease.js index 3ca95ad8cba..1d42fde416a 100644 --- a/docs/scripts/rerelease.js +++ b/docs/scripts/rerelease.js @@ -4,10 +4,12 @@ const fs = require('fs') const p = path.join(__dirname, '../versions.json') -fs.writeFile(p, JSON.stringify(require(p).filter((v) => v !== name)), function ( - err -) { - if (err) { - return console.error(err) +fs.writeFile( + p, + JSON.stringify(require(p).filter((v) => v !== name)), + function (err) { + if (err) { + return console.error(err) + } } -}) +) diff --git a/docs/sidebar.json b/docs/sidebar.json index 8db29416be0..3d22d0617b1 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -46,6 +46,7 @@ "items": [ "advanced", "guides/oauth2-clients", + "guides/oauth2-grant-type-jwt-bearer", "guides/common-oauth2-openid-connect-flows", "guides/using-oauth2", "guides/token-expiration", diff --git a/docs/src/theme/CodeFromRemote.js b/docs/src/theme/CodeFromRemote.js index a3609647f38..189028c2091 100644 --- a/docs/src/theme/CodeFromRemote.js +++ b/docs/src/theme/CodeFromRemote.js @@ -53,21 +53,23 @@ const findLine = (needle, haystack) => { return index } -const transform = ({ startAt, endAt }) => (content) => { - let lines = content.split('\n') +const transform = + ({ startAt, endAt }) => + (content) => { + let lines = content.split('\n') - const startIndex = findLine(startAt, lines) - if (startIndex > 0) { - lines = ['// ...', ...lines.slice(startIndex, -1)] - } + const startIndex = findLine(startAt, lines) + if (startIndex > 0) { + lines = ['// ...', ...lines.slice(startIndex, -1)] + } - const endIndex = findLine(endAt, lines) - if (endIndex > 0) { - lines = [...lines.slice(0, endIndex + 1), '// ...'] - } + const endIndex = findLine(endAt, lines) + if (endIndex > 0) { + lines = [...lines.slice(0, endIndex + 1), '// ...'] + } - return lines.join('\n') -} + return lines.join('\n') + } const CodeFromRemote = (props) => { const { src, title } = props diff --git a/docs/src/theme/ketoRelationTuplesPrism.js b/docs/src/theme/ketoRelationTuplesPrism.js index 513d653dfdd..9a00e5bbf70 100644 --- a/docs/src/theme/ketoRelationTuplesPrism.js +++ b/docs/src/theme/ketoRelationTuplesPrism.js @@ -44,7 +44,8 @@ export default (prism) => (prism.languages['keto-relation-tuples'] = { comment: /\/\/.*(\n|$)/, 'relation-tuple': { - pattern: /([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]+)@?((\(([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]*)\))|([^:#@()\n]+))/, + pattern: + /([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]+)@?((\(([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]*)\))|([^:#@()\n]+))/, inside: { namespace, object, diff --git a/driver/config/provider.go b/driver/config/provider.go index d06b8bb3d3b..be79fa931ff 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -65,6 +65,9 @@ const ( KeyOAuth2LegacyErrors = "oauth2.include_legacy_error_fields" KeyExcludeNotBeforeClaim = "oauth2.exclude_not_before_claim" KeyAllowedTopLevelClaims = "oauth2.allowed_top_level_claims" + KeyOAuth2GrantJWTIDOptional = "oauth2.grant.jwt.jti_optional" + KeyOAuth2GrantJWTIssuedDateOptional = "oauth2.grant.jwt.iat_optional" + KeyOAuth2GrantJWTMaxDuration = "oauth2.grant.jwt.max_ttl" KeyRefreshTokenHookURL = "oauth2.refresh_token_hook" // #nosec G101 ) @@ -433,3 +436,15 @@ func (p *Provider) CGroupsV1AutoMaxProcsEnabled() bool { func (p *Provider) GrantAllClientCredentialsScopesPerDefault() bool { return p.p.Bool(KeyGrantAllClientCredentialsScopesPerDefault) } + +func (p *Provider) GrantTypeJWTBearerIDOptional() bool { + return p.p.Bool(KeyOAuth2GrantJWTIDOptional) +} + +func (p *Provider) GrantTypeJWTBearerIssuedDateOptional() bool { + return p.p.Bool(KeyOAuth2GrantJWTIssuedDateOptional) +} + +func (p *Provider) GrantTypeJWTBearerMaxDuration() time.Duration { + return p.p.DurationF(KeyOAuth2GrantJWTMaxDuration, time.Hour*24*30) +} diff --git a/driver/registry.go b/driver/registry.go index 5947be13266..1309e00b59c 100644 --- a/driver/registry.go +++ b/driver/registry.go @@ -3,6 +3,8 @@ package driver import ( "context" + "github.com/ory/hydra/oauth2/trust" + "github.com/pkg/errors" "github.com/ory/x/errorsx" @@ -44,6 +46,7 @@ type Registry interface { client.Registry consent.Registry jwk.Registry + trust.Registry oauth2.Registry PrometheusManager() *prometheus.MetricsManager x.TracingProvider diff --git a/driver/registry_base.go b/driver/registry_base.go index 0928dc0a993..1def42d76ba 100644 --- a/driver/registry_base.go +++ b/driver/registry_base.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" + "github.com/ory/hydra/oauth2/trust" "github.com/ory/hydra/x/oauth2cors" "github.com/ory/hydra/persistence" @@ -46,6 +47,8 @@ type RegistryBase struct { C *config.Provider ch *client.Handler fh fosite.Hasher + jwtGrantH *trust.Handler + jwtGrantV *trust.GrantValidator kh *jwk.Handler cv *client.Validator hh *healthx.Handler @@ -108,6 +111,7 @@ func (m *RegistryBase) RegisterRoutes(admin *x.RouterAdmin, public *x.RouterPubl m.KeyHandler().SetRoutes(admin, public, m.OAuth2AwareMiddleware()) m.ClientHandler().SetRoutes(admin) m.OAuth2Handler().SetRoutes(admin, public, m.OAuth2AwareMiddleware()) + m.JWTGrantHandler().SetRoutes(admin) } func (m *RegistryBase) BuildVersion() string { @@ -187,6 +191,20 @@ func (m *RegistryBase) KeyHandler() *jwk.Handler { return m.kh } +func (m *RegistryBase) JWTGrantHandler() *trust.Handler { + if m.jwtGrantH == nil { + m.jwtGrantH = trust.NewHandler(m.r) + } + return m.jwtGrantH +} + +func (m *RegistryBase) GrantValidator() *trust.GrantValidator { + if m.jwtGrantV == nil { + m.jwtGrantV = trust.NewGrantValidator() + } + return m.jwtGrantV +} + func (m *RegistryBase) HealthHandler() *healthx.Handler { if m.hh == nil { m.hh = healthx.NewHandler(m.Writer(), m.buildVersion, healthx.ReadyCheckers{ @@ -258,20 +276,24 @@ func (m *RegistryBase) CookieStore() sessions.Store { func (m *RegistryBase) oAuth2Config() *compose.Config { return &compose.Config{ - AccessTokenLifespan: m.C.AccessTokenLifespan(), - RefreshTokenLifespan: m.C.RefreshTokenLifespan(), - AuthorizeCodeLifespan: m.C.AuthCodeLifespan(), - IDTokenLifespan: m.C.IDTokenLifespan(), - IDTokenIssuer: m.C.IssuerURL().String(), - HashCost: m.C.BCryptCost(), - ScopeStrategy: m.ScopeStrategy(), - SendDebugMessagesToClients: m.C.ShareOAuth2Debug(), - UseLegacyErrorFormat: m.C.OAuth2LegacyErrors(), - EnforcePKCE: m.C.PKCEEnforced(), - EnforcePKCEForPublicClients: m.C.EnforcePKCEForPublicClients(), - EnablePKCEPlainChallengeMethod: false, - TokenURL: urlx.AppendPaths(m.C.PublicURL(), oauth2.TokenPath).String(), - RedirectSecureChecker: x.IsRedirectURISecure(m.C), + AccessTokenLifespan: m.C.AccessTokenLifespan(), + RefreshTokenLifespan: m.C.RefreshTokenLifespan(), + AuthorizeCodeLifespan: m.C.AuthCodeLifespan(), + IDTokenLifespan: m.C.IDTokenLifespan(), + IDTokenIssuer: m.C.IssuerURL().String(), + HashCost: m.C.BCryptCost(), + ScopeStrategy: m.ScopeStrategy(), + SendDebugMessagesToClients: m.C.ShareOAuth2Debug(), + UseLegacyErrorFormat: m.C.OAuth2LegacyErrors(), + EnforcePKCE: m.C.PKCEEnforced(), + EnforcePKCEForPublicClients: m.C.EnforcePKCEForPublicClients(), + EnablePKCEPlainChallengeMethod: false, + TokenURL: urlx.AppendPaths(m.C.PublicURL(), oauth2.TokenPath).String(), + RedirectSecureChecker: x.IsRedirectURISecure(m.C), + GrantTypeJWTBearerCanSkipClientAuth: false, + GrantTypeJWTBearerIDOptional: m.C.GrantTypeJWTBearerIDOptional(), + GrantTypeJWTBearerIssuedDateOptional: m.C.GrantTypeJWTBearerIssuedDateOptional(), + GrantTypeJWTBearerMaxDuration: m.C.GrantTypeJWTBearerMaxDuration(), } } @@ -326,6 +348,7 @@ func (m *RegistryBase) OAuth2Provider() fosite.OAuth2Provider { compose.OAuth2TokenRevocationFactory, compose.OAuth2TokenIntrospectionFactory, compose.OAuth2PKCEFactory, + compose.RFC7523AssertionGrantFactory, ) } return m.fop diff --git a/driver/registry_sql.go b/driver/registry_sql.go index 53ed54377e9..e4801afa403 100644 --- a/driver/registry_sql.go +++ b/driver/registry_sql.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/ory/hydra/oauth2/trust" "github.com/ory/x/errorsx" "github.com/luna-duclos/instrumentedsql" @@ -123,3 +124,7 @@ func (m *RegistrySQL) OAuth2Storage() x.FositeStorer { func (m *RegistrySQL) KeyManager() jwk.Manager { return m.Persister() } + +func (m *RegistrySQL) GrantManager() trust.GrantManager { + return m.Persister() +} diff --git a/go.mod b/go.mod index 80cf4f449c7..2cb4ae6519f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ory/hydra go 1.16 replace ( + github.com/bradleyjkemp/cupaloy/v2 => github.com/aeneasr/cupaloy/v2 v2.6.1-0.20210924214125-3dfdd01210a3 github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.0.0 github.com/gobuffalo/packr => github.com/gobuffalo/packr v1.30.1 github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 @@ -14,6 +15,7 @@ replace ( ) require ( + github.com/bradleyjkemp/cupaloy/v2 v2.6.0 github.com/cenkalti/backoff/v3 v3.0.0 github.com/evanphx/json-patch v4.9.0+incompatible github.com/go-bindata/go-bindata v3.1.2+incompatible diff --git a/go.sum b/go.sum index a9b5c45ac74..17619c88086 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aeneasr/cupaloy/v2 v2.6.1-0.20210924214125-3dfdd01210a3 h1:/SkiUr3JJzun9QN9cpUVCPri2ZwOFJ3ani+F3vdoCiY= +github.com/aeneasr/cupaloy/v2 v2.6.1-0.20210924214125-3dfdd01210a3/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM= @@ -175,8 +177,6 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4Yn github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= -github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= diff --git a/internal/httpclient/client/admin/admin_client.go b/internal/httpclient/client/admin/admin_client.go index e626d4ccbdb..cec74f7cfc2 100644 --- a/internal/httpclient/client/admin/admin_client.go +++ b/internal/httpclient/client/admin/admin_client.go @@ -45,6 +45,10 @@ type ClientService interface { DeleteOAuth2Token(params *DeleteOAuth2TokenParams) (*DeleteOAuth2TokenNoContent, error) + DeleteTrustedJwtGrantIssuer(params *DeleteTrustedJwtGrantIssuerParams) (*DeleteTrustedJwtGrantIssuerNoContent, error) + + FlushInactiveJwtBearerGrants(params *FlushInactiveJwtBearerGrantsParams) (*FlushInactiveJwtBearerGrantsNoContent, error) + FlushInactiveOAuth2Tokens(params *FlushInactiveOAuth2TokensParams) (*FlushInactiveOAuth2TokensNoContent, error) GetConsentRequest(params *GetConsentRequestParams) (*GetConsentRequestOK, error) @@ -59,6 +63,8 @@ type ClientService interface { GetOAuth2Client(params *GetOAuth2ClientParams) (*GetOAuth2ClientOK, error) + GetTrustedJwtGrantIssuer(params *GetTrustedJwtGrantIssuerParams) (*GetTrustedJwtGrantIssuerOK, error) + GetVersion(params *GetVersionParams) (*GetVersionOK, error) IntrospectOAuth2Token(params *IntrospectOAuth2TokenParams) (*IntrospectOAuth2TokenOK, error) @@ -69,6 +75,8 @@ type ClientService interface { ListSubjectConsentSessions(params *ListSubjectConsentSessionsParams) (*ListSubjectConsentSessionsOK, error) + ListTrustedJwtGrantIssuers(params *ListTrustedJwtGrantIssuersParams) (*ListTrustedJwtGrantIssuersOK, error) + PatchOAuth2Client(params *PatchOAuth2ClientParams) (*PatchOAuth2ClientOK, error) RejectConsentRequest(params *RejectConsentRequestParams) (*RejectConsentRequestOK, error) @@ -81,6 +89,8 @@ type ClientService interface { RevokeConsentSessions(params *RevokeConsentSessionsParams) (*RevokeConsentSessionsNoContent, error) + TrustJwtGrantIssuer(params *TrustJwtGrantIssuerParams) (*TrustJwtGrantIssuerCreated, error) + UpdateJSONWebKey(params *UpdateJSONWebKeyParams) (*UpdateJSONWebKeyOK, error) UpdateJSONWebKeySet(params *UpdateJSONWebKeySetParams) (*UpdateJSONWebKeySetOK, error) @@ -454,6 +464,84 @@ func (a *Client) DeleteOAuth2Token(params *DeleteOAuth2TokenParams) (*DeleteOAut panic(msg) } +/* + DeleteTrustedJwtGrantIssuer deletes a trusted o auth2 j w t bearer grant type issuer + + Use this endpoint to delete trusted JWT Bearer Grant Type Issuer. The ID is the one returned when you +created the trust relationship. + +Once deleted, the associated issuer will no longer be able to perform the JSON Web Token (JWT) Profile +for OAuth 2.0 Client Authentication and Authorization Grant. +*/ +func (a *Client) DeleteTrustedJwtGrantIssuer(params *DeleteTrustedJwtGrantIssuerParams) (*DeleteTrustedJwtGrantIssuerNoContent, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewDeleteTrustedJwtGrantIssuerParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "deleteTrustedJwtGrantIssuer", + Method: "DELETE", + PathPattern: "/trust/grants/jwt-bearer/issuers/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &DeleteTrustedJwtGrantIssuerReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + success, ok := result.(*DeleteTrustedJwtGrantIssuerNoContent) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for deleteTrustedJwtGrantIssuer: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + +/* + FlushInactiveJwtBearerGrants flushes expired jwt bearer grants + + This endpoint flushes expired jwt-bearer grants from the database. You can set a time after which no tokens will be +not be touched, in case you want to keep recent tokens for auditing. Refresh tokens can not be flushed as they are deleted +automatically when performing the refresh flow. +*/ +func (a *Client) FlushInactiveJwtBearerGrants(params *FlushInactiveJwtBearerGrantsParams) (*FlushInactiveJwtBearerGrantsNoContent, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewFlushInactiveJwtBearerGrantsParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "flushInactiveJwtBearerGrants", + Method: "POST", + PathPattern: "/grants/jwt-bearer/flush", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &FlushInactiveJwtBearerGrantsReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + success, ok := result.(*FlushInactiveJwtBearerGrantsNoContent) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for flushInactiveJwtBearerGrants: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* FlushInactiveOAuth2Tokens flushes expired o auth2 access tokens @@ -727,6 +815,43 @@ func (a *Client) GetOAuth2Client(params *GetOAuth2ClientParams) (*GetOAuth2Clien panic(msg) } +/* + GetTrustedJwtGrantIssuer gets a trusted o auth2 j w t bearer grant type issuer + + Use this endpoint to get a trusted JWT Bearer Grant Type Issuer. The ID is the one returned when you +created the trust relationship. +*/ +func (a *Client) GetTrustedJwtGrantIssuer(params *GetTrustedJwtGrantIssuerParams) (*GetTrustedJwtGrantIssuerOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetTrustedJwtGrantIssuerParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "getTrustedJwtGrantIssuer", + Method: "GET", + PathPattern: "/trust/grants/jwt-bearer/issuers/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &GetTrustedJwtGrantIssuerReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + success, ok := result.(*GetTrustedJwtGrantIssuerOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for getTrustedJwtGrantIssuer: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* GetVersion gets service version @@ -931,6 +1056,42 @@ func (a *Client) ListSubjectConsentSessions(params *ListSubjectConsentSessionsPa panic(msg) } +/* + ListTrustedJwtGrantIssuers lists trusted o auth2 j w t bearer grant type issuers + + Use this endpoint to list all trusted JWT Bearer Grant Type Issuers. +*/ +func (a *Client) ListTrustedJwtGrantIssuers(params *ListTrustedJwtGrantIssuersParams) (*ListTrustedJwtGrantIssuersOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewListTrustedJwtGrantIssuersParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "listTrustedJwtGrantIssuers", + Method: "GET", + PathPattern: "/trust/grants/jwt-bearer/issuers", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &ListTrustedJwtGrantIssuersReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + success, ok := result.(*ListTrustedJwtGrantIssuersOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for listTrustedJwtGrantIssuers: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* PatchOAuth2Client patches an o auth 2 0 client @@ -1180,6 +1341,44 @@ func (a *Client) RevokeConsentSessions(params *RevokeConsentSessionsParams) (*Re panic(msg) } +/* + TrustJwtGrantIssuer trusts an o auth2 j w t bearer grant type issuer + + Use this endpoint to establish a trust relationship for a JWT issuer +to perform JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication +and Authorization Grants [RFC7523](https://datatracker.ietf.org/doc/html/rfc7523). +*/ +func (a *Client) TrustJwtGrantIssuer(params *TrustJwtGrantIssuerParams) (*TrustJwtGrantIssuerCreated, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewTrustJwtGrantIssuerParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "trustJwtGrantIssuer", + Method: "POST", + PathPattern: "/trust/grants/jwt-bearer/issuers", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &TrustJwtGrantIssuerReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + success, ok := result.(*TrustJwtGrantIssuerCreated) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for trustJwtGrantIssuer: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* UpdateJSONWebKey updates a JSON web key diff --git a/internal/httpclient/client/admin/delete_trusted_jwt_grant_issuer_parameters.go b/internal/httpclient/client/admin/delete_trusted_jwt_grant_issuer_parameters.go new file mode 100644 index 00000000000..7e9757530cf --- /dev/null +++ b/internal/httpclient/client/admin/delete_trusted_jwt_grant_issuer_parameters.go @@ -0,0 +1,135 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewDeleteTrustedJwtGrantIssuerParams creates a new DeleteTrustedJwtGrantIssuerParams object +// with the default values initialized. +func NewDeleteTrustedJwtGrantIssuerParams() *DeleteTrustedJwtGrantIssuerParams { + var () + return &DeleteTrustedJwtGrantIssuerParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewDeleteTrustedJwtGrantIssuerParamsWithTimeout creates a new DeleteTrustedJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewDeleteTrustedJwtGrantIssuerParamsWithTimeout(timeout time.Duration) *DeleteTrustedJwtGrantIssuerParams { + var () + return &DeleteTrustedJwtGrantIssuerParams{ + + timeout: timeout, + } +} + +// NewDeleteTrustedJwtGrantIssuerParamsWithContext creates a new DeleteTrustedJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a context for a request +func NewDeleteTrustedJwtGrantIssuerParamsWithContext(ctx context.Context) *DeleteTrustedJwtGrantIssuerParams { + var () + return &DeleteTrustedJwtGrantIssuerParams{ + + Context: ctx, + } +} + +// NewDeleteTrustedJwtGrantIssuerParamsWithHTTPClient creates a new DeleteTrustedJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewDeleteTrustedJwtGrantIssuerParamsWithHTTPClient(client *http.Client) *DeleteTrustedJwtGrantIssuerParams { + var () + return &DeleteTrustedJwtGrantIssuerParams{ + HTTPClient: client, + } +} + +/*DeleteTrustedJwtGrantIssuerParams contains all the parameters to send to the API endpoint +for the delete trusted jwt grant issuer operation typically these are written to a http.Request +*/ +type DeleteTrustedJwtGrantIssuerParams struct { + + /*ID + The id of the desired grant + + */ + ID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) WithTimeout(timeout time.Duration) *DeleteTrustedJwtGrantIssuerParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) WithContext(ctx context.Context) *DeleteTrustedJwtGrantIssuerParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) WithHTTPClient(client *http.Client) *DeleteTrustedJwtGrantIssuerParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithID adds the id to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) WithID(id string) *DeleteTrustedJwtGrantIssuerParams { + o.SetID(id) + return o +} + +// SetID adds the id to the delete trusted jwt grant issuer params +func (o *DeleteTrustedJwtGrantIssuerParams) SetID(id string) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *DeleteTrustedJwtGrantIssuerParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param id + if err := r.SetPathParam("id", o.ID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/client/admin/delete_trusted_jwt_grant_issuer_responses.go b/internal/httpclient/client/admin/delete_trusted_jwt_grant_issuer_responses.go new file mode 100644 index 00000000000..4dbc61b2ea2 --- /dev/null +++ b/internal/httpclient/client/admin/delete_trusted_jwt_grant_issuer_responses.go @@ -0,0 +1,136 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// DeleteTrustedJwtGrantIssuerReader is a Reader for the DeleteTrustedJwtGrantIssuer structure. +type DeleteTrustedJwtGrantIssuerReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *DeleteTrustedJwtGrantIssuerReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 204: + result := NewDeleteTrustedJwtGrantIssuerNoContent() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 404: + result := NewDeleteTrustedJwtGrantIssuerNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewDeleteTrustedJwtGrantIssuerInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + + default: + return nil, runtime.NewAPIError("unknown error", response, response.Code()) + } +} + +// NewDeleteTrustedJwtGrantIssuerNoContent creates a DeleteTrustedJwtGrantIssuerNoContent with default headers values +func NewDeleteTrustedJwtGrantIssuerNoContent() *DeleteTrustedJwtGrantIssuerNoContent { + return &DeleteTrustedJwtGrantIssuerNoContent{} +} + +/*DeleteTrustedJwtGrantIssuerNoContent handles this case with default header values. + +Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is +typically 201. +*/ +type DeleteTrustedJwtGrantIssuerNoContent struct { +} + +func (o *DeleteTrustedJwtGrantIssuerNoContent) Error() string { + return fmt.Sprintf("[DELETE /trust/grants/jwt-bearer/issuers/{id}][%d] deleteTrustedJwtGrantIssuerNoContent ", 204) +} + +func (o *DeleteTrustedJwtGrantIssuerNoContent) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + +// NewDeleteTrustedJwtGrantIssuerNotFound creates a DeleteTrustedJwtGrantIssuerNotFound with default headers values +func NewDeleteTrustedJwtGrantIssuerNotFound() *DeleteTrustedJwtGrantIssuerNotFound { + return &DeleteTrustedJwtGrantIssuerNotFound{} +} + +/*DeleteTrustedJwtGrantIssuerNotFound handles this case with default header values. + +genericError +*/ +type DeleteTrustedJwtGrantIssuerNotFound struct { + Payload *models.GenericError +} + +func (o *DeleteTrustedJwtGrantIssuerNotFound) Error() string { + return fmt.Sprintf("[DELETE /trust/grants/jwt-bearer/issuers/{id}][%d] deleteTrustedJwtGrantIssuerNotFound %+v", 404, o.Payload) +} + +func (o *DeleteTrustedJwtGrantIssuerNotFound) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *DeleteTrustedJwtGrantIssuerNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewDeleteTrustedJwtGrantIssuerInternalServerError creates a DeleteTrustedJwtGrantIssuerInternalServerError with default headers values +func NewDeleteTrustedJwtGrantIssuerInternalServerError() *DeleteTrustedJwtGrantIssuerInternalServerError { + return &DeleteTrustedJwtGrantIssuerInternalServerError{} +} + +/*DeleteTrustedJwtGrantIssuerInternalServerError handles this case with default header values. + +genericError +*/ +type DeleteTrustedJwtGrantIssuerInternalServerError struct { + Payload *models.GenericError +} + +func (o *DeleteTrustedJwtGrantIssuerInternalServerError) Error() string { + return fmt.Sprintf("[DELETE /trust/grants/jwt-bearer/issuers/{id}][%d] deleteTrustedJwtGrantIssuerInternalServerError %+v", 500, o.Payload) +} + +func (o *DeleteTrustedJwtGrantIssuerInternalServerError) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *DeleteTrustedJwtGrantIssuerInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/internal/httpclient/client/admin/flush_inactive_jwt_bearer_grants_parameters.go b/internal/httpclient/client/admin/flush_inactive_jwt_bearer_grants_parameters.go new file mode 100644 index 00000000000..f2e1a202f50 --- /dev/null +++ b/internal/httpclient/client/admin/flush_inactive_jwt_bearer_grants_parameters.go @@ -0,0 +1,135 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// NewFlushInactiveJwtBearerGrantsParams creates a new FlushInactiveJwtBearerGrantsParams object +// with the default values initialized. +func NewFlushInactiveJwtBearerGrantsParams() *FlushInactiveJwtBearerGrantsParams { + var () + return &FlushInactiveJwtBearerGrantsParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewFlushInactiveJwtBearerGrantsParamsWithTimeout creates a new FlushInactiveJwtBearerGrantsParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewFlushInactiveJwtBearerGrantsParamsWithTimeout(timeout time.Duration) *FlushInactiveJwtBearerGrantsParams { + var () + return &FlushInactiveJwtBearerGrantsParams{ + + timeout: timeout, + } +} + +// NewFlushInactiveJwtBearerGrantsParamsWithContext creates a new FlushInactiveJwtBearerGrantsParams object +// with the default values initialized, and the ability to set a context for a request +func NewFlushInactiveJwtBearerGrantsParamsWithContext(ctx context.Context) *FlushInactiveJwtBearerGrantsParams { + var () + return &FlushInactiveJwtBearerGrantsParams{ + + Context: ctx, + } +} + +// NewFlushInactiveJwtBearerGrantsParamsWithHTTPClient creates a new FlushInactiveJwtBearerGrantsParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewFlushInactiveJwtBearerGrantsParamsWithHTTPClient(client *http.Client) *FlushInactiveJwtBearerGrantsParams { + var () + return &FlushInactiveJwtBearerGrantsParams{ + HTTPClient: client, + } +} + +/*FlushInactiveJwtBearerGrantsParams contains all the parameters to send to the API endpoint +for the flush inactive jwt bearer grants operation typically these are written to a http.Request +*/ +type FlushInactiveJwtBearerGrantsParams struct { + + /*Body*/ + Body *models.FlushInactiveJwtBearerGrantsParams + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) WithTimeout(timeout time.Duration) *FlushInactiveJwtBearerGrantsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) WithContext(ctx context.Context) *FlushInactiveJwtBearerGrantsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) WithHTTPClient(client *http.Client) *FlushInactiveJwtBearerGrantsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) WithBody(body *models.FlushInactiveJwtBearerGrantsParams) *FlushInactiveJwtBearerGrantsParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the flush inactive jwt bearer grants params +func (o *FlushInactiveJwtBearerGrantsParams) SetBody(body *models.FlushInactiveJwtBearerGrantsParams) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *FlushInactiveJwtBearerGrantsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if o.Body != nil { + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/client/admin/flush_inactive_jwt_bearer_grants_responses.go b/internal/httpclient/client/admin/flush_inactive_jwt_bearer_grants_responses.go new file mode 100644 index 00000000000..2fce8d47424 --- /dev/null +++ b/internal/httpclient/client/admin/flush_inactive_jwt_bearer_grants_responses.go @@ -0,0 +1,97 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// FlushInactiveJwtBearerGrantsReader is a Reader for the FlushInactiveJwtBearerGrants structure. +type FlushInactiveJwtBearerGrantsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *FlushInactiveJwtBearerGrantsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 204: + result := NewFlushInactiveJwtBearerGrantsNoContent() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 500: + result := NewFlushInactiveJwtBearerGrantsInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + + default: + return nil, runtime.NewAPIError("unknown error", response, response.Code()) + } +} + +// NewFlushInactiveJwtBearerGrantsNoContent creates a FlushInactiveJwtBearerGrantsNoContent with default headers values +func NewFlushInactiveJwtBearerGrantsNoContent() *FlushInactiveJwtBearerGrantsNoContent { + return &FlushInactiveJwtBearerGrantsNoContent{} +} + +/*FlushInactiveJwtBearerGrantsNoContent handles this case with default header values. + +Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is +typically 201. +*/ +type FlushInactiveJwtBearerGrantsNoContent struct { +} + +func (o *FlushInactiveJwtBearerGrantsNoContent) Error() string { + return fmt.Sprintf("[POST /grants/jwt-bearer/flush][%d] flushInactiveJwtBearerGrantsNoContent ", 204) +} + +func (o *FlushInactiveJwtBearerGrantsNoContent) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + +// NewFlushInactiveJwtBearerGrantsInternalServerError creates a FlushInactiveJwtBearerGrantsInternalServerError with default headers values +func NewFlushInactiveJwtBearerGrantsInternalServerError() *FlushInactiveJwtBearerGrantsInternalServerError { + return &FlushInactiveJwtBearerGrantsInternalServerError{} +} + +/*FlushInactiveJwtBearerGrantsInternalServerError handles this case with default header values. + +genericError +*/ +type FlushInactiveJwtBearerGrantsInternalServerError struct { + Payload *models.GenericError +} + +func (o *FlushInactiveJwtBearerGrantsInternalServerError) Error() string { + return fmt.Sprintf("[POST /grants/jwt-bearer/flush][%d] flushInactiveJwtBearerGrantsInternalServerError %+v", 500, o.Payload) +} + +func (o *FlushInactiveJwtBearerGrantsInternalServerError) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *FlushInactiveJwtBearerGrantsInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/internal/httpclient/client/admin/get_trusted_jwt_grant_issuer_parameters.go b/internal/httpclient/client/admin/get_trusted_jwt_grant_issuer_parameters.go new file mode 100644 index 00000000000..e317e2fbacf --- /dev/null +++ b/internal/httpclient/client/admin/get_trusted_jwt_grant_issuer_parameters.go @@ -0,0 +1,135 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewGetTrustedJwtGrantIssuerParams creates a new GetTrustedJwtGrantIssuerParams object +// with the default values initialized. +func NewGetTrustedJwtGrantIssuerParams() *GetTrustedJwtGrantIssuerParams { + var () + return &GetTrustedJwtGrantIssuerParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewGetTrustedJwtGrantIssuerParamsWithTimeout creates a new GetTrustedJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewGetTrustedJwtGrantIssuerParamsWithTimeout(timeout time.Duration) *GetTrustedJwtGrantIssuerParams { + var () + return &GetTrustedJwtGrantIssuerParams{ + + timeout: timeout, + } +} + +// NewGetTrustedJwtGrantIssuerParamsWithContext creates a new GetTrustedJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a context for a request +func NewGetTrustedJwtGrantIssuerParamsWithContext(ctx context.Context) *GetTrustedJwtGrantIssuerParams { + var () + return &GetTrustedJwtGrantIssuerParams{ + + Context: ctx, + } +} + +// NewGetTrustedJwtGrantIssuerParamsWithHTTPClient creates a new GetTrustedJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewGetTrustedJwtGrantIssuerParamsWithHTTPClient(client *http.Client) *GetTrustedJwtGrantIssuerParams { + var () + return &GetTrustedJwtGrantIssuerParams{ + HTTPClient: client, + } +} + +/*GetTrustedJwtGrantIssuerParams contains all the parameters to send to the API endpoint +for the get trusted jwt grant issuer operation typically these are written to a http.Request +*/ +type GetTrustedJwtGrantIssuerParams struct { + + /*ID + The id of the desired grant + + */ + ID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) WithTimeout(timeout time.Duration) *GetTrustedJwtGrantIssuerParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) WithContext(ctx context.Context) *GetTrustedJwtGrantIssuerParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) WithHTTPClient(client *http.Client) *GetTrustedJwtGrantIssuerParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithID adds the id to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) WithID(id string) *GetTrustedJwtGrantIssuerParams { + o.SetID(id) + return o +} + +// SetID adds the id to the get trusted jwt grant issuer params +func (o *GetTrustedJwtGrantIssuerParams) SetID(id string) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *GetTrustedJwtGrantIssuerParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param id + if err := r.SetPathParam("id", o.ID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/client/admin/get_trusted_jwt_grant_issuer_responses.go b/internal/httpclient/client/admin/get_trusted_jwt_grant_issuer_responses.go new file mode 100644 index 00000000000..b9c60e66b5d --- /dev/null +++ b/internal/httpclient/client/admin/get_trusted_jwt_grant_issuer_responses.go @@ -0,0 +1,147 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// GetTrustedJwtGrantIssuerReader is a Reader for the GetTrustedJwtGrantIssuer structure. +type GetTrustedJwtGrantIssuerReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetTrustedJwtGrantIssuerReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetTrustedJwtGrantIssuerOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 404: + result := NewGetTrustedJwtGrantIssuerNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewGetTrustedJwtGrantIssuerInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + + default: + return nil, runtime.NewAPIError("unknown error", response, response.Code()) + } +} + +// NewGetTrustedJwtGrantIssuerOK creates a GetTrustedJwtGrantIssuerOK with default headers values +func NewGetTrustedJwtGrantIssuerOK() *GetTrustedJwtGrantIssuerOK { + return &GetTrustedJwtGrantIssuerOK{} +} + +/*GetTrustedJwtGrantIssuerOK handles this case with default header values. + +trustedJwtGrantIssuer +*/ +type GetTrustedJwtGrantIssuerOK struct { + Payload *models.TrustedJwtGrantIssuer +} + +func (o *GetTrustedJwtGrantIssuerOK) Error() string { + return fmt.Sprintf("[GET /trust/grants/jwt-bearer/issuers/{id}][%d] getTrustedJwtGrantIssuerOK %+v", 200, o.Payload) +} + +func (o *GetTrustedJwtGrantIssuerOK) GetPayload() *models.TrustedJwtGrantIssuer { + return o.Payload +} + +func (o *GetTrustedJwtGrantIssuerOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.TrustedJwtGrantIssuer) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetTrustedJwtGrantIssuerNotFound creates a GetTrustedJwtGrantIssuerNotFound with default headers values +func NewGetTrustedJwtGrantIssuerNotFound() *GetTrustedJwtGrantIssuerNotFound { + return &GetTrustedJwtGrantIssuerNotFound{} +} + +/*GetTrustedJwtGrantIssuerNotFound handles this case with default header values. + +genericError +*/ +type GetTrustedJwtGrantIssuerNotFound struct { + Payload *models.GenericError +} + +func (o *GetTrustedJwtGrantIssuerNotFound) Error() string { + return fmt.Sprintf("[GET /trust/grants/jwt-bearer/issuers/{id}][%d] getTrustedJwtGrantIssuerNotFound %+v", 404, o.Payload) +} + +func (o *GetTrustedJwtGrantIssuerNotFound) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *GetTrustedJwtGrantIssuerNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetTrustedJwtGrantIssuerInternalServerError creates a GetTrustedJwtGrantIssuerInternalServerError with default headers values +func NewGetTrustedJwtGrantIssuerInternalServerError() *GetTrustedJwtGrantIssuerInternalServerError { + return &GetTrustedJwtGrantIssuerInternalServerError{} +} + +/*GetTrustedJwtGrantIssuerInternalServerError handles this case with default header values. + +genericError +*/ +type GetTrustedJwtGrantIssuerInternalServerError struct { + Payload *models.GenericError +} + +func (o *GetTrustedJwtGrantIssuerInternalServerError) Error() string { + return fmt.Sprintf("[GET /trust/grants/jwt-bearer/issuers/{id}][%d] getTrustedJwtGrantIssuerInternalServerError %+v", 500, o.Payload) +} + +func (o *GetTrustedJwtGrantIssuerInternalServerError) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *GetTrustedJwtGrantIssuerInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/internal/httpclient/client/admin/list_trusted_jwt_grant_issuers_parameters.go b/internal/httpclient/client/admin/list_trusted_jwt_grant_issuers_parameters.go new file mode 100644 index 00000000000..2d924cded2d --- /dev/null +++ b/internal/httpclient/client/admin/list_trusted_jwt_grant_issuers_parameters.go @@ -0,0 +1,211 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewListTrustedJwtGrantIssuersParams creates a new ListTrustedJwtGrantIssuersParams object +// with the default values initialized. +func NewListTrustedJwtGrantIssuersParams() *ListTrustedJwtGrantIssuersParams { + var () + return &ListTrustedJwtGrantIssuersParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewListTrustedJwtGrantIssuersParamsWithTimeout creates a new ListTrustedJwtGrantIssuersParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewListTrustedJwtGrantIssuersParamsWithTimeout(timeout time.Duration) *ListTrustedJwtGrantIssuersParams { + var () + return &ListTrustedJwtGrantIssuersParams{ + + timeout: timeout, + } +} + +// NewListTrustedJwtGrantIssuersParamsWithContext creates a new ListTrustedJwtGrantIssuersParams object +// with the default values initialized, and the ability to set a context for a request +func NewListTrustedJwtGrantIssuersParamsWithContext(ctx context.Context) *ListTrustedJwtGrantIssuersParams { + var () + return &ListTrustedJwtGrantIssuersParams{ + + Context: ctx, + } +} + +// NewListTrustedJwtGrantIssuersParamsWithHTTPClient creates a new ListTrustedJwtGrantIssuersParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewListTrustedJwtGrantIssuersParamsWithHTTPClient(client *http.Client) *ListTrustedJwtGrantIssuersParams { + var () + return &ListTrustedJwtGrantIssuersParams{ + HTTPClient: client, + } +} + +/*ListTrustedJwtGrantIssuersParams contains all the parameters to send to the API endpoint +for the list trusted jwt grant issuers operation typically these are written to a http.Request +*/ +type ListTrustedJwtGrantIssuersParams struct { + + /*Issuer + If optional "issuer" is supplied, only jwt-bearer grants with this issuer will be returned. + + */ + Issuer *string + /*Limit + The maximum amount of policies returned, upper bound is 500 policies + + */ + Limit *int64 + /*Offset + The offset from where to start looking. + + */ + Offset *int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) WithTimeout(timeout time.Duration) *ListTrustedJwtGrantIssuersParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) WithContext(ctx context.Context) *ListTrustedJwtGrantIssuersParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) WithHTTPClient(client *http.Client) *ListTrustedJwtGrantIssuersParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithIssuer adds the issuer to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) WithIssuer(issuer *string) *ListTrustedJwtGrantIssuersParams { + o.SetIssuer(issuer) + return o +} + +// SetIssuer adds the issuer to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) SetIssuer(issuer *string) { + o.Issuer = issuer +} + +// WithLimit adds the limit to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) WithLimit(limit *int64) *ListTrustedJwtGrantIssuersParams { + o.SetLimit(limit) + return o +} + +// SetLimit adds the limit to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) SetLimit(limit *int64) { + o.Limit = limit +} + +// WithOffset adds the offset to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) WithOffset(offset *int64) *ListTrustedJwtGrantIssuersParams { + o.SetOffset(offset) + return o +} + +// SetOffset adds the offset to the list trusted jwt grant issuers params +func (o *ListTrustedJwtGrantIssuersParams) SetOffset(offset *int64) { + o.Offset = offset +} + +// WriteToRequest writes these params to a swagger request +func (o *ListTrustedJwtGrantIssuersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if o.Issuer != nil { + + // query param issuer + var qrIssuer string + if o.Issuer != nil { + qrIssuer = *o.Issuer + } + qIssuer := qrIssuer + if qIssuer != "" { + if err := r.SetQueryParam("issuer", qIssuer); err != nil { + return err + } + } + + } + + if o.Limit != nil { + + // query param limit + var qrLimit int64 + if o.Limit != nil { + qrLimit = *o.Limit + } + qLimit := swag.FormatInt64(qrLimit) + if qLimit != "" { + if err := r.SetQueryParam("limit", qLimit); err != nil { + return err + } + } + + } + + if o.Offset != nil { + + // query param offset + var qrOffset int64 + if o.Offset != nil { + qrOffset = *o.Offset + } + qOffset := swag.FormatInt64(qrOffset) + if qOffset != "" { + if err := r.SetQueryParam("offset", qOffset); err != nil { + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/client/admin/list_trusted_jwt_grant_issuers_responses.go b/internal/httpclient/client/admin/list_trusted_jwt_grant_issuers_responses.go new file mode 100644 index 00000000000..8cb7d4e3e82 --- /dev/null +++ b/internal/httpclient/client/admin/list_trusted_jwt_grant_issuers_responses.go @@ -0,0 +1,106 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// ListTrustedJwtGrantIssuersReader is a Reader for the ListTrustedJwtGrantIssuers structure. +type ListTrustedJwtGrantIssuersReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *ListTrustedJwtGrantIssuersReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewListTrustedJwtGrantIssuersOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 500: + result := NewListTrustedJwtGrantIssuersInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + + default: + return nil, runtime.NewAPIError("unknown error", response, response.Code()) + } +} + +// NewListTrustedJwtGrantIssuersOK creates a ListTrustedJwtGrantIssuersOK with default headers values +func NewListTrustedJwtGrantIssuersOK() *ListTrustedJwtGrantIssuersOK { + return &ListTrustedJwtGrantIssuersOK{} +} + +/*ListTrustedJwtGrantIssuersOK handles this case with default header values. + +trustedJwtGrantIssuers +*/ +type ListTrustedJwtGrantIssuersOK struct { + Payload models.TrustedJwtGrantIssuers +} + +func (o *ListTrustedJwtGrantIssuersOK) Error() string { + return fmt.Sprintf("[GET /trust/grants/jwt-bearer/issuers][%d] listTrustedJwtGrantIssuersOK %+v", 200, o.Payload) +} + +func (o *ListTrustedJwtGrantIssuersOK) GetPayload() models.TrustedJwtGrantIssuers { + return o.Payload +} + +func (o *ListTrustedJwtGrantIssuersOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewListTrustedJwtGrantIssuersInternalServerError creates a ListTrustedJwtGrantIssuersInternalServerError with default headers values +func NewListTrustedJwtGrantIssuersInternalServerError() *ListTrustedJwtGrantIssuersInternalServerError { + return &ListTrustedJwtGrantIssuersInternalServerError{} +} + +/*ListTrustedJwtGrantIssuersInternalServerError handles this case with default header values. + +genericError +*/ +type ListTrustedJwtGrantIssuersInternalServerError struct { + Payload *models.GenericError +} + +func (o *ListTrustedJwtGrantIssuersInternalServerError) Error() string { + return fmt.Sprintf("[GET /trust/grants/jwt-bearer/issuers][%d] listTrustedJwtGrantIssuersInternalServerError %+v", 500, o.Payload) +} + +func (o *ListTrustedJwtGrantIssuersInternalServerError) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *ListTrustedJwtGrantIssuersInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/internal/httpclient/client/admin/trust_jwt_grant_issuer_parameters.go b/internal/httpclient/client/admin/trust_jwt_grant_issuer_parameters.go new file mode 100644 index 00000000000..d89b9766684 --- /dev/null +++ b/internal/httpclient/client/admin/trust_jwt_grant_issuer_parameters.go @@ -0,0 +1,135 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// NewTrustJwtGrantIssuerParams creates a new TrustJwtGrantIssuerParams object +// with the default values initialized. +func NewTrustJwtGrantIssuerParams() *TrustJwtGrantIssuerParams { + var () + return &TrustJwtGrantIssuerParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewTrustJwtGrantIssuerParamsWithTimeout creates a new TrustJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewTrustJwtGrantIssuerParamsWithTimeout(timeout time.Duration) *TrustJwtGrantIssuerParams { + var () + return &TrustJwtGrantIssuerParams{ + + timeout: timeout, + } +} + +// NewTrustJwtGrantIssuerParamsWithContext creates a new TrustJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a context for a request +func NewTrustJwtGrantIssuerParamsWithContext(ctx context.Context) *TrustJwtGrantIssuerParams { + var () + return &TrustJwtGrantIssuerParams{ + + Context: ctx, + } +} + +// NewTrustJwtGrantIssuerParamsWithHTTPClient creates a new TrustJwtGrantIssuerParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewTrustJwtGrantIssuerParamsWithHTTPClient(client *http.Client) *TrustJwtGrantIssuerParams { + var () + return &TrustJwtGrantIssuerParams{ + HTTPClient: client, + } +} + +/*TrustJwtGrantIssuerParams contains all the parameters to send to the API endpoint +for the trust jwt grant issuer operation typically these are written to a http.Request +*/ +type TrustJwtGrantIssuerParams struct { + + /*Body*/ + Body *models.TrustJwtGrantIssuerBody + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) WithTimeout(timeout time.Duration) *TrustJwtGrantIssuerParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) WithContext(ctx context.Context) *TrustJwtGrantIssuerParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) WithHTTPClient(client *http.Client) *TrustJwtGrantIssuerParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) WithBody(body *models.TrustJwtGrantIssuerBody) *TrustJwtGrantIssuerParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the trust jwt grant issuer params +func (o *TrustJwtGrantIssuerParams) SetBody(body *models.TrustJwtGrantIssuerBody) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *TrustJwtGrantIssuerParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if o.Body != nil { + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/client/admin/trust_jwt_grant_issuer_responses.go b/internal/httpclient/client/admin/trust_jwt_grant_issuer_responses.go new file mode 100644 index 00000000000..e18993e4626 --- /dev/null +++ b/internal/httpclient/client/admin/trust_jwt_grant_issuer_responses.go @@ -0,0 +1,186 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package admin + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/ory/hydra/internal/httpclient/models" +) + +// TrustJwtGrantIssuerReader is a Reader for the TrustJwtGrantIssuer structure. +type TrustJwtGrantIssuerReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *TrustJwtGrantIssuerReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 201: + result := NewTrustJwtGrantIssuerCreated() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewTrustJwtGrantIssuerBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 409: + result := NewTrustJwtGrantIssuerConflict() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewTrustJwtGrantIssuerInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + + default: + return nil, runtime.NewAPIError("unknown error", response, response.Code()) + } +} + +// NewTrustJwtGrantIssuerCreated creates a TrustJwtGrantIssuerCreated with default headers values +func NewTrustJwtGrantIssuerCreated() *TrustJwtGrantIssuerCreated { + return &TrustJwtGrantIssuerCreated{} +} + +/*TrustJwtGrantIssuerCreated handles this case with default header values. + +trustedJwtGrantIssuer +*/ +type TrustJwtGrantIssuerCreated struct { + Payload *models.TrustedJwtGrantIssuer +} + +func (o *TrustJwtGrantIssuerCreated) Error() string { + return fmt.Sprintf("[POST /trust/grants/jwt-bearer/issuers][%d] trustJwtGrantIssuerCreated %+v", 201, o.Payload) +} + +func (o *TrustJwtGrantIssuerCreated) GetPayload() *models.TrustedJwtGrantIssuer { + return o.Payload +} + +func (o *TrustJwtGrantIssuerCreated) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.TrustedJwtGrantIssuer) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewTrustJwtGrantIssuerBadRequest creates a TrustJwtGrantIssuerBadRequest with default headers values +func NewTrustJwtGrantIssuerBadRequest() *TrustJwtGrantIssuerBadRequest { + return &TrustJwtGrantIssuerBadRequest{} +} + +/*TrustJwtGrantIssuerBadRequest handles this case with default header values. + +genericError +*/ +type TrustJwtGrantIssuerBadRequest struct { + Payload *models.GenericError +} + +func (o *TrustJwtGrantIssuerBadRequest) Error() string { + return fmt.Sprintf("[POST /trust/grants/jwt-bearer/issuers][%d] trustJwtGrantIssuerBadRequest %+v", 400, o.Payload) +} + +func (o *TrustJwtGrantIssuerBadRequest) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *TrustJwtGrantIssuerBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewTrustJwtGrantIssuerConflict creates a TrustJwtGrantIssuerConflict with default headers values +func NewTrustJwtGrantIssuerConflict() *TrustJwtGrantIssuerConflict { + return &TrustJwtGrantIssuerConflict{} +} + +/*TrustJwtGrantIssuerConflict handles this case with default header values. + +genericError +*/ +type TrustJwtGrantIssuerConflict struct { + Payload *models.GenericError +} + +func (o *TrustJwtGrantIssuerConflict) Error() string { + return fmt.Sprintf("[POST /trust/grants/jwt-bearer/issuers][%d] trustJwtGrantIssuerConflict %+v", 409, o.Payload) +} + +func (o *TrustJwtGrantIssuerConflict) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *TrustJwtGrantIssuerConflict) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewTrustJwtGrantIssuerInternalServerError creates a TrustJwtGrantIssuerInternalServerError with default headers values +func NewTrustJwtGrantIssuerInternalServerError() *TrustJwtGrantIssuerInternalServerError { + return &TrustJwtGrantIssuerInternalServerError{} +} + +/*TrustJwtGrantIssuerInternalServerError handles this case with default header values. + +genericError +*/ +type TrustJwtGrantIssuerInternalServerError struct { + Payload *models.GenericError +} + +func (o *TrustJwtGrantIssuerInternalServerError) Error() string { + return fmt.Sprintf("[POST /trust/grants/jwt-bearer/issuers][%d] trustJwtGrantIssuerInternalServerError %+v", 500, o.Payload) +} + +func (o *TrustJwtGrantIssuerInternalServerError) GetPayload() *models.GenericError { + return o.Payload +} + +func (o *TrustJwtGrantIssuerInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GenericError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/internal/httpclient/models/flush_inactive_jwt_bearer_grants_params.go b/internal/httpclient/models/flush_inactive_jwt_bearer_grants_params.go new file mode 100644 index 00000000000..9c5e6908766 --- /dev/null +++ b/internal/httpclient/models/flush_inactive_jwt_bearer_grants_params.go @@ -0,0 +1,69 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// FlushInactiveJwtBearerGrantsParams flush inactive jwt bearer grants params +// +// swagger:model flushInactiveJwtBearerGrantsParams +type FlushInactiveJwtBearerGrantsParams struct { + + // The "notAfter" sets after which point grants should not be flushed. This is useful when you want to keep a history + // of recently added grants. + // Format: date-time + NotAfter strfmt.DateTime `json:"notAfter,omitempty"` +} + +// Validate validates this flush inactive jwt bearer grants params +func (m *FlushInactiveJwtBearerGrantsParams) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateNotAfter(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *FlushInactiveJwtBearerGrantsParams) validateNotAfter(formats strfmt.Registry) error { + + if swag.IsZero(m.NotAfter) { // not required + return nil + } + + if err := validate.FormatOf("notAfter", "body", "date-time", m.NotAfter.String(), formats); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *FlushInactiveJwtBearerGrantsParams) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *FlushInactiveJwtBearerGrantsParams) UnmarshalBinary(b []byte) error { + var res FlushInactiveJwtBearerGrantsParams + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/httpclient/models/generic_error.go b/internal/httpclient/models/generic_error.go new file mode 100644 index 00000000000..8033e159a2e --- /dev/null +++ b/internal/httpclient/models/generic_error.go @@ -0,0 +1,90 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// GenericError generic error +// +// swagger:model genericError +type GenericError struct { + + // The status code + Code int64 `json:"code,omitempty"` + + // Debug information + // + // This field is often not exposed to protect against leaking + // sensitive information. + Debug string `json:"debug,omitempty"` + + // Further error details + Details interface{} `json:"details,omitempty"` + + // Error message + // + // The error's message. + // Required: true + Message *string `json:"message"` + + // A human-readable reason for the error + Reason string `json:"reason,omitempty"` + + // The request ID + // + // The request ID is often exposed internally in order to trace + // errors across service architectures. This is often a UUID. + Request string `json:"request,omitempty"` + + // The status description + Status string `json:"status,omitempty"` +} + +// Validate validates this generic error +func (m *GenericError) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateMessage(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *GenericError) validateMessage(formats strfmt.Registry) error { + + if err := validate.Required("message", "body", m.Message); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *GenericError) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *GenericError) UnmarshalBinary(b []byte) error { + var res GenericError + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/httpclient/models/trust_jwt_grant_issuer_body.go b/internal/httpclient/models/trust_jwt_grant_issuer_body.go new file mode 100644 index 00000000000..09573619d17 --- /dev/null +++ b/internal/httpclient/models/trust_jwt_grant_issuer_body.go @@ -0,0 +1,146 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// TrustJwtGrantIssuerBody trust jwt grant issuer body +// +// swagger:model trustJwtGrantIssuerBody +type TrustJwtGrantIssuerBody struct { + + // The "expires_at" indicates, when grant will expire, so we will reject assertion from "issuer" targeting "subject". + // Required: true + // Format: date-time + ExpiresAt *strfmt.DateTime `json:"expires_at"` + + // The "issuer" identifies the principal that issued the JWT assertion (same as "iss" claim in JWT). + // Required: true + Issuer *string `json:"issuer"` + + // jwk + // Required: true + Jwk *JSONWebKey `json:"jwk"` + + // The "scope" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) + // Required: true + Scope []string `json:"scope"` + + // The "subject" identifies the principal that is the subject of the JWT. + // Required: true + Subject *string `json:"subject"` +} + +// Validate validates this trust jwt grant issuer body +func (m *TrustJwtGrantIssuerBody) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateExpiresAt(formats); err != nil { + res = append(res, err) + } + + if err := m.validateIssuer(formats); err != nil { + res = append(res, err) + } + + if err := m.validateJwk(formats); err != nil { + res = append(res, err) + } + + if err := m.validateScope(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSubject(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *TrustJwtGrantIssuerBody) validateExpiresAt(formats strfmt.Registry) error { + + if err := validate.Required("expires_at", "body", m.ExpiresAt); err != nil { + return err + } + + if err := validate.FormatOf("expires_at", "body", "date-time", m.ExpiresAt.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *TrustJwtGrantIssuerBody) validateIssuer(formats strfmt.Registry) error { + + if err := validate.Required("issuer", "body", m.Issuer); err != nil { + return err + } + + return nil +} + +func (m *TrustJwtGrantIssuerBody) validateJwk(formats strfmt.Registry) error { + + if err := validate.Required("jwk", "body", m.Jwk); err != nil { + return err + } + + if m.Jwk != nil { + if err := m.Jwk.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("jwk") + } + return err + } + } + + return nil +} + +func (m *TrustJwtGrantIssuerBody) validateScope(formats strfmt.Registry) error { + + if err := validate.Required("scope", "body", m.Scope); err != nil { + return err + } + + return nil +} + +func (m *TrustJwtGrantIssuerBody) validateSubject(formats strfmt.Registry) error { + + if err := validate.Required("subject", "body", m.Subject); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *TrustJwtGrantIssuerBody) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *TrustJwtGrantIssuerBody) UnmarshalBinary(b []byte) error { + var res TrustJwtGrantIssuerBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/httpclient/models/trusted_json_web_key.go b/internal/httpclient/models/trusted_json_web_key.go new file mode 100644 index 00000000000..e12666cfa96 --- /dev/null +++ b/internal/httpclient/models/trusted_json_web_key.go @@ -0,0 +1,46 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// TrustedJSONWebKey trusted Json web key +// +// swagger:model trustedJsonWebKey +type TrustedJSONWebKey struct { + + // The "key_id" is key unique identifier (same as kid header in jws/jwt). + Kid string `json:"kid,omitempty"` + + // The "set" is basically a name for a group(set) of keys. Will be the same as "issuer" in grant. + Set string `json:"set,omitempty"` +} + +// Validate validates this trusted Json web key +func (m *TrustedJSONWebKey) Validate(formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *TrustedJSONWebKey) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *TrustedJSONWebKey) UnmarshalBinary(b []byte) error { + var res TrustedJSONWebKey + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/httpclient/models/trusted_jwt_grant_issuer.go b/internal/httpclient/models/trusted_jwt_grant_issuer.go new file mode 100644 index 00000000000..577f867d553 --- /dev/null +++ b/internal/httpclient/models/trusted_jwt_grant_issuer.go @@ -0,0 +1,126 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// TrustedJwtGrantIssuer trusted jwt grant issuer +// +// swagger:model trustedJwtGrantIssuer +type TrustedJwtGrantIssuer struct { + + // The "created_at" indicates, when grant was created. + // Format: date-time + CreatedAt strfmt.DateTime `json:"created_at,omitempty"` + + // The "expires_at" indicates, when grant will expire, so we will reject assertion from "issuer" targeting "subject". + // Format: date-time + ExpiresAt strfmt.DateTime `json:"expires_at,omitempty"` + + // id + ID string `json:"id,omitempty"` + + // The "issuer" identifies the principal that issued the JWT assertion (same as "iss" claim in JWT). + Issuer string `json:"issuer,omitempty"` + + // public key + PublicKey *TrustedJSONWebKey `json:"public_key,omitempty"` + + // The "scope" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) + Scope []string `json:"scope"` + + // The "subject" identifies the principal that is the subject of the JWT. + Subject string `json:"subject,omitempty"` +} + +// Validate validates this trusted jwt grant issuer +func (m *TrustedJwtGrantIssuer) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCreatedAt(formats); err != nil { + res = append(res, err) + } + + if err := m.validateExpiresAt(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePublicKey(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *TrustedJwtGrantIssuer) validateCreatedAt(formats strfmt.Registry) error { + + if swag.IsZero(m.CreatedAt) { // not required + return nil + } + + if err := validate.FormatOf("created_at", "body", "date-time", m.CreatedAt.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *TrustedJwtGrantIssuer) validateExpiresAt(formats strfmt.Registry) error { + + if swag.IsZero(m.ExpiresAt) { // not required + return nil + } + + if err := validate.FormatOf("expires_at", "body", "date-time", m.ExpiresAt.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *TrustedJwtGrantIssuer) validatePublicKey(formats strfmt.Registry) error { + + if swag.IsZero(m.PublicKey) { // not required + return nil + } + + if m.PublicKey != nil { + if err := m.PublicKey.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("public_key") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *TrustedJwtGrantIssuer) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *TrustedJwtGrantIssuer) UnmarshalBinary(b []byte) error { + var res TrustedJwtGrantIssuer + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/httpclient/models/trusted_jwt_grant_issuers.go b/internal/httpclient/models/trusted_jwt_grant_issuers.go new file mode 100644 index 00000000000..a8d4d3859f1 --- /dev/null +++ b/internal/httpclient/models/trusted_jwt_grant_issuers.go @@ -0,0 +1,45 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// TrustedJwtGrantIssuers trusted jwt grant issuers +// +// swagger:model trustedJwtGrantIssuers +type TrustedJwtGrantIssuers []*TrustedJwtGrantIssuer + +// Validate validates this trusted jwt grant issuers +func (m TrustedJwtGrantIssuers) Validate(formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + if swag.IsZero(m[i]) { // not required + continue + } + + if m[i] != nil { + if err := m[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/models/volume.go b/internal/httpclient/models/volume.go index d777e291958..f278b8ac30b 100644 --- a/internal/httpclient/models/volume.go +++ b/internal/httpclient/models/volume.go @@ -40,8 +40,7 @@ type Volume struct { // Required: true Options map[string]string `json:"Options"` - // The level at which the volume exists. Either `global` for cluster-wide, - // or `local` for machine level. + // The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. // Required: true Scope *string `json:"Scope"` diff --git a/internal/testhelpers/janitor_test_helper.go b/internal/testhelpers/janitor_test_helper.go index 93d3fee9d91..ebb66464dba 100644 --- a/internal/testhelpers/janitor_test_helper.go +++ b/internal/testhelpers/janitor_test_helper.go @@ -7,7 +7,9 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" @@ -17,6 +19,7 @@ import ( "github.com/ory/hydra/driver/config" "github.com/ory/hydra/internal" "github.com/ory/hydra/oauth2" + "github.com/ory/hydra/oauth2/trust" "github.com/ory/hydra/x" "github.com/ory/x/logrusx" @@ -29,12 +32,19 @@ type JanitorConsentTestHelper struct { flushConsentRequests []*consent.ConsentRequest flushAccessRequests []*fosite.Request flushRefreshRequests []*fosite.AccessRequest + flushGrants []*createGrantRequest conf *config.Provider Lifespan time.Duration } +type createGrantRequest struct { + grant trust.Grant + pk jose.JSONWebKey +} + +const lifespan = time.Hour + func NewConsentJanitorTestHelper(uniqueName string) *JanitorConsentTestHelper { - var lifespan = time.Hour conf := internal.NewConfigurationWithDefaults() conf.MustSet(config.KeyScopeStrategy, "DEPRECATED_HIERARCHICAL_SCOPE_STRATEGY") conf.MustSet(config.KeyIssuerURL, "http://hydra.localhost") @@ -50,6 +60,7 @@ func NewConsentJanitorTestHelper(uniqueName string) *JanitorConsentTestHelper { flushConsentRequests: genConsentRequests(uniqueName, lifespan), flushAccessRequests: getAccessRequests(uniqueName, lifespan), flushRefreshRequests: getRefreshRequests(uniqueName, lifespan), + flushGrants: getGrantRequests(uniqueName, lifespan), Lifespan: lifespan, } } @@ -134,6 +145,37 @@ func (j *JanitorConsentTestHelper) RefreshTokenNotAfterValidate(ctx context.Cont } } +func (j *JanitorConsentTestHelper) GrantNotAfterSetup(ctx context.Context, cl client.Manager, gr trust.GrantManager) func(t *testing.T) { + return func(t *testing.T) { + for _, fg := range j.flushGrants { + require.NoError(t, gr.CreateGrant(ctx, fg.grant, fg.pk)) + } + } +} + +func (j *JanitorConsentTestHelper) GrantNotAfterValidate(ctx context.Context, notAfter time.Time, gr trust.GrantManager) func(t *testing.T) { + return func(t *testing.T) { + var err error + + // flush won't delete grants that have not yet expired, so use now to check that + deleteUntil := time.Now().Round(time.Second) + if deleteUntil.After(notAfter) { + deleteUntil = notAfter + } + + for _, r := range j.flushGrants { + t.Logf("grant flush check: %s", r.grant.Issuer) + _, err = gr.GetConcreteGrant(ctx, r.grant.ID) + + if deleteUntil.After(r.grant.ExpiresAt) { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + } +} + func (j *JanitorConsentTestHelper) LoginRejectionSetup(ctx context.Context, cm consent.Manager, cl client.Manager) func(t *testing.T) { return func(t *testing.T) { var err error @@ -711,3 +753,62 @@ func genConsentRequests(uniqueName string, lifespan time.Duration) []*consent.Co }, } } + +func getGrantRequests(uniqueName string, lifespan time.Duration) []*createGrantRequest { + return []*createGrantRequest{ + { + grant: trust.Grant{ + ID: uuid.New().String(), + Issuer: fmt.Sprintf("%s_flush-grant-iss-1", uniqueName), + Subject: fmt.Sprintf("%s_flush-grant-sub-1", uniqueName), + Scope: []string{"foo", "bar"}, + PublicKey: trust.PublicKey{ + Set: fmt.Sprintf("%s_flush-grant-iss-1", uniqueName), + KeyID: fmt.Sprintf("%s_flush-grant-kid-1", uniqueName), + }, + CreatedAt: time.Now().Round(time.Second), + ExpiresAt: time.Now().Round(time.Second).Add(lifespan), + }, + pk: jose.JSONWebKey{ + Key: []byte("asdf"), + KeyID: fmt.Sprintf("%s_flush-grant-kid-1", uniqueName), + }, + }, + { + grant: trust.Grant{ + ID: uuid.New().String(), + Issuer: fmt.Sprintf("%s_flush-grant-iss-2", uniqueName), + Subject: fmt.Sprintf("%s_flush-grant-sub-2", uniqueName), + Scope: []string{"foo", "bar"}, + PublicKey: trust.PublicKey{ + Set: fmt.Sprintf("%s_flush-grant-iss-2", uniqueName), + KeyID: fmt.Sprintf("%s_flush-grant-kid-2", uniqueName), + }, + CreatedAt: time.Now().Round(time.Second).Add(-(lifespan + time.Minute)), + ExpiresAt: time.Now().Round(time.Second).Add(-(lifespan + time.Minute)).Add(lifespan), + }, + pk: jose.JSONWebKey{ + Key: []byte("asdf"), + KeyID: fmt.Sprintf("%s_flush-grant-kid-2", uniqueName), + }, + }, + { + grant: trust.Grant{ + ID: uuid.New().String(), + Issuer: fmt.Sprintf("%s_flush-grant-iss-3", uniqueName), + Subject: fmt.Sprintf("%s_flush-grant-sub-3", uniqueName), + Scope: []string{"foo", "bar"}, + PublicKey: trust.PublicKey{ + Set: fmt.Sprintf("%s_flush-grant-iss-3", uniqueName), + KeyID: fmt.Sprintf("%s_flush-grant-kid-3", uniqueName), + }, + CreatedAt: time.Now().Round(time.Second).Add(-(lifespan + time.Hour)), + ExpiresAt: time.Now().Round(time.Second).Add(-(lifespan + time.Hour)).Add(lifespan), + }, + pk: jose.JSONWebKey{ + Key: []byte("asdf"), + KeyID: fmt.Sprintf("%s_flush-grant-kid-3", uniqueName), + }, + }, + } +} diff --git a/oauth2/fosite_store_helpers.go b/oauth2/fosite_store_helpers.go index 262bd9557c5..2c17284e663 100644 --- a/oauth2/fosite_store_helpers.go +++ b/oauth2/fosite_store_helpers.go @@ -29,6 +29,11 @@ import ( "time" "github.com/gobuffalo/pop/v5" + "gopkg.in/square/go-jose.v2" + + "github.com/ory/fosite/handler/rfc7523" + + "github.com/ory/hydra/oauth2/trust" "github.com/ory/hydra/x" @@ -180,6 +185,7 @@ func TestHelperRunner(t *testing.T, store InternalRegistry, k string) { t.Run(fmt.Sprintf("case=testFositeStoreClientAssertionJWTValid/db=%s", k), testFositeStoreClientAssertionJWTValid(store)) t.Run(fmt.Sprintf("case=testHelperDeleteAccessTokens/db=%s", k), testHelperDeleteAccessTokens(store)) t.Run(fmt.Sprintf("case=testHelperRevokeAccessToken/db=%s", k), testHelperRevokeAccessToken(store)) + t.Run(fmt.Sprintf("case=testFositeJWTBearerGrantStorage/db=%s", k), testFositeJWTBearerGrantStorage(store)) } func testHelperRequestIDMultiples(m InternalRegistry, _ string) func(t *testing.T) { @@ -724,6 +730,175 @@ func testFositeStoreClientAssertionJWTValid(m InternalRegistry) func(*testing.T) } } +func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { + return func(t *testing.T) { + grantManager := x.GrantManager() + keyManager := x.KeyManager() + keyGenerators := x.KeyGenerators() + keyGenerator, ok := keyGenerators[string(jose.RS256)] + require.True(t, ok) + grantStorage := x.OAuth2Storage().(rfc7523.RFC7523KeyStorage) + + t.Run("case=associated key added with grant", func(t *testing.T) { + keySet, err := keyGenerator.Generate("token-service-key", "sig") + require.NoError(t, err) + + publicKey := keySet.Keys[1] + issuer := "token-service" + subject := "bob@example.com" + grant := trust.Grant{ + ID: uuid.New(), + Issuer: issuer, + Subject: subject, + Scope: []string{"openid", "offline"}, + PublicKey: trust.PublicKey{Set: issuer, KeyID: publicKey.KeyID}, + CreatedAt: time.Now().UTC().Round(time.Second), + ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), + } + + storedKeySet, err := grantStorage.GetPublicKeys(context.TODO(), issuer, subject) + require.NoError(t, err) + require.Len(t, storedKeySet.Keys, 0) + + err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + require.NoError(t, err) + + storedKeySet, err = grantStorage.GetPublicKeys(context.TODO(), issuer, subject) + require.NoError(t, err) + assert.Len(t, storedKeySet.Keys, 1) + + storedKey, err := grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + require.NoError(t, err) + assert.Equal(t, publicKey.KeyID, storedKey.KeyID) + assert.Equal(t, publicKey.Use, storedKey.Use) + assert.Equal(t, publicKey.Key, storedKey.Key) + + storedScopes, err := grantStorage.GetPublicKeyScopes(context.TODO(), issuer, subject, publicKey.KeyID) + require.NoError(t, err) + assert.Equal(t, grant.Scope, storedScopes) + + storedKeySet, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + require.NoError(t, err) + assert.Equal(t, publicKey.KeyID, storedKeySet.Keys[0].KeyID) + assert.Equal(t, publicKey.Use, storedKeySet.Keys[0].Use) + assert.Equal(t, publicKey.Key, storedKeySet.Keys[0].Key) + }) + + t.Run("case=only associated key returns", func(t *testing.T) { + keySet, err := keyGenerator.Generate("some-key", "sig") + require.NoError(t, err) + + err = keyManager.AddKeySet(context.TODO(), "some-set", keySet) + require.NoError(t, err) + + keySet, err = keyGenerator.Generate("maria-key", "sig") + require.NoError(t, err) + + publicKey := keySet.Keys[1] + issuer := "maria" + subject := "maria@example.com" + grant := trust.Grant{ + ID: uuid.New(), + Issuer: issuer, + Subject: subject, + Scope: []string{"openid"}, + PublicKey: trust.PublicKey{Set: issuer, KeyID: publicKey.KeyID}, + CreatedAt: time.Now().UTC().Round(time.Second), + ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), + } + + err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + require.NoError(t, err) + + storedKeySet, err := grantStorage.GetPublicKeys(context.TODO(), issuer, subject) + require.NoError(t, err) + assert.Len(t, storedKeySet.Keys, 1) + assert.Equal(t, publicKey.KeyID, storedKeySet.Keys[0].KeyID) + assert.Equal(t, publicKey.Use, storedKeySet.Keys[0].Use) + assert.Equal(t, publicKey.Key, storedKeySet.Keys[0].Key) + + storedKeySet, err = grantStorage.GetPublicKeys(context.TODO(), issuer, "non-existing-subject") + require.NoError(t, err) + assert.Len(t, storedKeySet.Keys, 0) + + _, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, "non-existing-subject", publicKey.KeyID) + require.Error(t, err) + }) + + t.Run("case=associated key is deleted, when granted is deleted", func(t *testing.T) { + keySet, err := keyGenerator.Generate("hackerman-key", "sig") + require.NoError(t, err) + + publicKey := keySet.Keys[1] + issuer := "aeneas" + subject := "aeneas@example.com" + grant := trust.Grant{ + ID: uuid.New(), + Issuer: issuer, + Subject: subject, + Scope: []string{"openid", "offline"}, + PublicKey: trust.PublicKey{Set: issuer, KeyID: publicKey.KeyID}, + CreatedAt: time.Now().UTC().Round(time.Second), + ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), + } + + err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + require.NoError(t, err) + + _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, grant.PublicKey.KeyID) + require.NoError(t, err) + + _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + require.NoError(t, err) + + err = grantManager.DeleteGrant(context.TODO(), grant.ID) + require.NoError(t, err) + + _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + assert.Error(t, err) + + _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + assert.Error(t, err) + }) + + t.Run("case=associated grant is deleted, when key is deleted", func(t *testing.T) { + keySet, err := keyGenerator.Generate("vladimir-key", "sig") + require.NoError(t, err) + + publicKey := keySet.Keys[1] + issuer := "vladimir" + subject := "vladimir@example.com" + grant := trust.Grant{ + ID: uuid.New(), + Issuer: issuer, + Subject: subject, + Scope: []string{"openid", "offline"}, + PublicKey: trust.PublicKey{Set: issuer, KeyID: publicKey.KeyID}, + CreatedAt: time.Now().UTC().Round(time.Second), + ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), + } + + err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + require.NoError(t, err) + + _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + require.NoError(t, err) + + _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + require.NoError(t, err) + + err = keyManager.DeleteKey(context.TODO(), issuer, publicKey.KeyID) + require.NoError(t, err) + + _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + assert.Error(t, err) + + _, err = grantManager.GetConcreteGrant(context.TODO(), grant.ID) + assert.Error(t, err) + }) + } +} + func doTestCommit(m InternalRegistry, t *testing.T, createFn func(context.Context, string, fosite.Requester) error, getFn func(context.Context, string, fosite.Session) (fosite.Requester, error), diff --git a/oauth2/registry.go b/oauth2/registry.go index 00eb0ea1740..5895b26a7d7 100644 --- a/oauth2/registry.go +++ b/oauth2/registry.go @@ -6,11 +6,14 @@ import ( "github.com/ory/hydra/client" "github.com/ory/hydra/consent" "github.com/ory/hydra/jwk" + "github.com/ory/hydra/oauth2/trust" "github.com/ory/hydra/x" ) type InternalRegistry interface { client.Registry + jwk.Registry + trust.Registry x.RegistryWriter x.RegistryLogger consent.Registry diff --git a/oauth2/trust/doc.go b/oauth2/trust/doc.go new file mode 100644 index 00000000000..d71339e9679 --- /dev/null +++ b/oauth2/trust/doc.go @@ -0,0 +1,134 @@ +/* + * Copyright © 2015-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + * + * @author Aeneas Rekkas + * @copyright 2015-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +// Package trust implements jwt-bearer grant management capabilities +// +// JWT-Bearer Grant represents resource owner (RO) permission for client to act on behalf of the RO using jwt. +// Client uses jwt to request access token to act as RO. +package trust + +import ( + "time" + + "github.com/ory/hydra/x" +) + +// swagger:model trustJwtGrantIssuerBody +type trustJwtGrantIssuerBody struct { + // The "issuer" identifies the principal that issued the JWT assertion (same as "iss" claim in JWT). + // + // required: true + // example: https://jwt-idp.example.com + Issuer string `json:"issuer"` + + // The "subject" identifies the principal that is the subject of the JWT. + // + // required:true + // example: mike@example.com + Subject string `json:"subject"` + + // The "scope" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) + // + // required:true + // example: ["openid", "offline"] + Scope []string `json:"scope"` + + // The "jwk" contains public key in JWK format issued by "issuer", that will be used to check JWT assertion signature. + // + // required:true + JWK x.JSONWebKey `json:"jwk"` + + // The "expires_at" indicates, when grant will expire, so we will reject assertion from "issuer" targeting "subject". + // + // required:true + ExpiresAt time.Time `json:"expires_at"` +} + +// swagger:parameters trustJwtGrantIssuer +type trustJwtGrantIssuer struct { + // in: body + Body trustJwtGrantIssuerBody +} + +// swagger:parameters listTrustedJwtGrantIssuers +type listTrustedJwtGrantIssuers struct { + // If optional "issuer" is supplied, only jwt-bearer grants with this issuer will be returned. + // + // in: query + // required: false + Issuer string `json:"issuer"` + + // The maximum amount of policies returned, upper bound is 500 policies + // in: query + Limit int `json:"limit"` + + // The offset from where to start looking. + // in: query + Offset int `json:"offset"` +} + +// swagger:parameters getTrustedJwtGrantIssuer deleteTrustedJwtGrantIssuer +type getTrustedJwtGrantIssuer struct { + // The id of the desired grant + // in: path + // required: true + ID string `json:"id"` +} + +// swagger:model trustedJwtGrantIssuers +type trustedJwtGrantIssuers []trustedJwtGrantIssuer + +// swagger:model trustedJwtGrantIssuer +type trustedJwtGrantIssuer struct { + // example: 9edc811f-4e28-453c-9b46-4de65f00217f + ID string `json:"id"` + + // The "issuer" identifies the principal that issued the JWT assertion (same as "iss" claim in JWT). + // example: https://jwt-idp.example.com + Issuer string `json:"issuer"` + + // The "subject" identifies the principal that is the subject of the JWT. + // example: mike@example.com + Subject string `json:"subject"` + + // The "scope" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) + // example: ["openid", "offline"] + Scope []string `json:"scope"` + + // The "public_key" contains information about public key issued by "issuer", that will be used to check JWT assertion signature. + PublicKey trustedJsonWebKey `json:"public_key"` + + // The "created_at" indicates, when grant was created. + CreatedAt time.Time `json:"created_at"` + + // The "expires_at" indicates, when grant will expire, so we will reject assertion from "issuer" targeting "subject". + ExpiresAt time.Time `json:"expires_at"` +} + +// swagger:model trustedJsonWebKey +type trustedJsonWebKey struct { + // The "set" is basically a name for a group(set) of keys. Will be the same as "issuer" in grant. + // example: https://jwt-idp.example.com + Set string `json:"set"` + + // The "key_id" is key unique identifier (same as kid header in jws/jwt). + // example: 123e4567-e89b-12d3-a456-426655440000 + KeyID string `json:"kid"` +} diff --git a/oauth2/trust/error.go b/oauth2/trust/error.go new file mode 100644 index 00000000000..5a2f5f9f2fb --- /dev/null +++ b/oauth2/trust/error.go @@ -0,0 +1,13 @@ +package trust + +import ( + "net/http" + + "github.com/ory/fosite" +) + +var ErrMissingRequiredParameter = &fosite.RFC6749Error{ + DescriptionField: "One of the required parameters is missing. Check your request parameters.", + ErrorField: "missing_required_parameter", + CodeField: http.StatusBadRequest, +} diff --git a/oauth2/trust/grant.go b/oauth2/trust/grant.go new file mode 100644 index 00000000000..7996a2995bd --- /dev/null +++ b/oauth2/trust/grant.go @@ -0,0 +1,35 @@ +package trust + +import ( + "time" +) + +type Grant struct { + ID string `json:"id"` + + // Issuer identifies the principal that issued the JWT assertion (same as iss claim in jwt). + Issuer string `json:"issuer"` + + // Subject identifies the principal that is the subject of the JWT. + Subject string `json:"subject"` + + // Scope contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) + Scope []string `json:"scope"` + + // PublicKeys contains information about public key issued by Issuer, that will be used to check JWT assertion signature. + PublicKey PublicKey `json:"public_key"` + + // CreatedAt indicates, when grant was created. + CreatedAt time.Time `json:"created_at"` + + // ExpiresAt indicates, when grant will expire, so we will reject assertion from Issuer targeting Subject. + ExpiresAt time.Time `json:"expires_at"` +} + +type PublicKey struct { + // Set is basically a name for a group(set) of keys. Will be the same as Issuer in grant. + Set string `json:"set"` + + // KeyID is key unique identifier (same as kid header in jws/jwt). + KeyID string `json:"kid"` +} diff --git a/oauth2/trust/handler.go b/oauth2/trust/handler.go new file mode 100644 index 00000000000..19631116c2f --- /dev/null +++ b/oauth2/trust/handler.go @@ -0,0 +1,195 @@ +package trust + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/google/uuid" + + "github.com/ory/x/errorsx" + "github.com/ory/x/pagination" + + "github.com/ory/hydra/x" + + "github.com/julienschmidt/httprouter" +) + +const ( + grantJWTBearerPath = "/trust/grants/jwt-bearer/issuers" +) + +type Handler struct { + registry InternalRegistry +} + +func NewHandler(r InternalRegistry) *Handler { + return &Handler{registry: r} +} + +func (h *Handler) SetRoutes(admin *x.RouterAdmin) { + admin.GET(grantJWTBearerPath+"/:id", h.Get) + admin.GET(grantJWTBearerPath, h.List) + admin.POST(grantJWTBearerPath, h.Create) + admin.DELETE(grantJWTBearerPath+"/:id", h.Delete) +} + +// swagger:route POST /trust/grants/jwt-bearer/issuers admin trustJwtGrantIssuer +// +// Trust an OAuth2 JWT Bearer Grant Type Issuer +// +// Use this endpoint to establish a trust relationship for a JWT issuer +// to perform JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication +// and Authorization Grants [RFC7523](https://datatracker.ietf.org/doc/html/rfc7523). +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 201: trustedJwtGrantIssuer +// 400: genericError +// 409: genericError +// 500: genericError +func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var grantRequest createGrantRequest + + if err := json.NewDecoder(r.Body).Decode(&grantRequest); err != nil { + h.registry.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + + if err := h.registry.GrantValidator().Validate(grantRequest); err != nil { + h.registry.Writer().WriteError(w, r, err) + return + } + + grant := Grant{ + ID: uuid.New().String(), + Issuer: grantRequest.Issuer, + Subject: grantRequest.Subject, + Scope: grantRequest.Scope, + PublicKey: PublicKey{ + Set: grantRequest.Issuer, // group all keys by issuer, so set=issuer + KeyID: grantRequest.PublicKeyJWK.KeyID, + }, + CreatedAt: time.Now().UTC().Round(time.Second), + ExpiresAt: grantRequest.ExpiresAt.UTC().Round(time.Second), + } + + if err := h.registry.GrantManager().CreateGrant(r.Context(), grant, grantRequest.PublicKeyJWK); err != nil { + h.registry.Writer().WriteError(w, r, err) + return + } + + h.registry.Writer().WriteCreated(w, r, grantJWTBearerPath+"/"+grant.ID, &grant) +} + +// swagger:route GET /trust/grants/jwt-bearer/issuers/{id} admin getTrustedJwtGrantIssuer +// +// Get a Trusted OAuth2 JWT Bearer Grant Type Issuer +// +// Use this endpoint to get a trusted JWT Bearer Grant Type Issuer. The ID is the one returned when you +// created the trust relationship. +/// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: trustedJwtGrantIssuer +// 404: genericError +// 500: genericError +func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + var id = ps.ByName("id") + + grant, err := h.registry.GrantManager().GetConcreteGrant(r.Context(), id) + if err != nil { + h.registry.Writer().WriteError(w, r, err) + return + } + + h.registry.Writer().Write(w, r, grant) +} + +// swagger:route DELETE /trust/grants/jwt-bearer/issuers/{id} admin deleteTrustedJwtGrantIssuer +// +// Delete a Trusted OAuth2 JWT Bearer Grant Type Issuer +// +// Use this endpoint to delete trusted JWT Bearer Grant Type Issuer. The ID is the one returned when you +// created the trust relationship. +// +// Once deleted, the associated issuer will no longer be able to perform the JSON Web Token (JWT) Profile +// for OAuth 2.0 Client Authentication and Authorization Grant. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 404: genericError +// 500: genericError +func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + var id = ps.ByName("id") + + if err := h.registry.GrantManager().DeleteGrant(r.Context(), id); err != nil { + h.registry.Writer().WriteError(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// swagger:route GET /trust/grants/jwt-bearer/issuers admin listTrustedJwtGrantIssuers +// +// List Trusted OAuth2 JWT Bearer Grant Type Issuers +// +// Use this endpoint to list all trusted JWT Bearer Grant Type Issuers. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: trustedJwtGrantIssuers +// 500: genericError +func (h *Handler) List(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + limit, offset := pagination.Parse(r, 100, 0, 500) + optionalIssuer := r.URL.Query().Get("issuer") + + grants, err := h.registry.GrantManager().GetGrants(r.Context(), limit, offset, optionalIssuer) + if err != nil { + h.registry.Writer().WriteError(w, r, err) + return + } + + n, err := h.registry.GrantManager().CountGrants(r.Context()) + if err != nil { + h.registry.Writer().WriteError(w, r, err) + return + } + + pagination.Header(w, r.URL, n, limit, offset) + if grants == nil { + grants = []Grant{} + } + + h.registry.Writer().Write(w, r, grants) +} diff --git a/oauth2/trust/handler_test.go b/oauth2/trust/handler_test.go new file mode 100644 index 00000000000..8b7ee977b2f --- /dev/null +++ b/oauth2/trust/handler_test.go @@ -0,0 +1,281 @@ +package trust_test + +import ( + "crypto/rand" + "crypto/rsa" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ory/hydra/oauth2/trust" + + "github.com/go-openapi/strfmt" + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + "gopkg.in/square/go-jose.v2" + + "github.com/ory/hydra/driver" + "github.com/ory/hydra/jwk" + + "github.com/ory/hydra/driver/config" + "github.com/ory/hydra/internal" + hydra "github.com/ory/hydra/internal/httpclient/client" + "github.com/ory/hydra/internal/httpclient/client/admin" + "github.com/ory/hydra/internal/httpclient/models" + "github.com/ory/hydra/x" + "github.com/ory/x/urlx" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context. +type HandlerTestSuite struct { + suite.Suite + registry driver.Registry + server *httptest.Server + hydraClient *hydra.OryHydra + publicKey *rsa.PublicKey +} + +// Setup will run before the tests in the suite are run. +func (s *HandlerTestSuite) SetupSuite() { + conf := internal.NewConfigurationWithDefaults() + conf.MustSet(config.KeySubjectTypesSupported, []string{"public"}) + conf.MustSet(config.KeyDefaultClientScope, []string{"foo", "bar"}) + s.registry = internal.NewRegistryMemory(s.T(), conf) + + router := x.NewRouterAdmin() + handler := trust.NewHandler(s.registry) + handler.SetRoutes(router) + jwkHandler := jwk.NewHandler(s.registry, conf) + jwkHandler.SetRoutes(router, x.NewRouterPublic(), func(h http.Handler) http.Handler { + return h + }) + s.server = httptest.NewServer(router) + + s.hydraClient = hydra.NewHTTPClientWithConfig(nil, &hydra.TransportConfig{Schemes: []string{"http"}, Host: urlx.ParseOrPanic(s.server.URL).Host}) + s.publicKey = s.generatePublicKey() +} + +// Setup before each test. +func (s *HandlerTestSuite) SetupTest() { +} + +// Will run after all the tests in the suite have been run. +func (s *HandlerTestSuite) TearDownSuite() { +} + +// Will run after each test in the suite. +func (s *HandlerTestSuite) TearDownTest() { + internal.CleanAndMigrate(s.registry)(s.T()) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run. +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} + +func (s *HandlerTestSuite) TestGrantCanBeCreatedAndFetched() { + createRequestParams := s.newCreateJwtBearerGrantParams( + "ory", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + model := createRequestParams.Body + + createResult, err := s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + + s.Require().NoError(err, "no errors expected on grant creation") + s.NotEmpty(createResult.Payload.ID, " grant id expected to be non-empty") + s.Equal(*model.Issuer, createResult.Payload.Issuer, "issuer must match") + s.Equal(*model.Subject, createResult.Payload.Subject, "subject must match") + s.Equal(model.Scope, createResult.Payload.Scope, "scopes must match") + s.Equal(*model.Issuer, createResult.Payload.PublicKey.Set, "public key set must match grant issuer") + s.Equal(*model.Jwk.Kid, createResult.Payload.PublicKey.Kid, "public key id must match") + s.Equal(model.ExpiresAt.String(), createResult.Payload.ExpiresAt.String(), "expiration date must match") + + getRequestParams := admin.NewGetTrustedJwtGrantIssuerParams() + getRequestParams.ID = createResult.Payload.ID + getResult, err := s.hydraClient.Admin.GetTrustedJwtGrantIssuer(getRequestParams) + + s.Require().NoError(err, "no errors expected on grant fetching") + s.Equal(getRequestParams.ID, getResult.Payload.ID, " grant id must match") + s.Equal(*model.Issuer, getResult.Payload.Issuer, "issuer must match") + s.Equal(*model.Subject, getResult.Payload.Subject, "subject must match") + s.Equal(model.Scope, getResult.Payload.Scope, "scopes must match") + s.Equal(*model.Issuer, getResult.Payload.PublicKey.Set, "public key set must match grant issuer") + s.Equal(*model.Jwk.Kid, getResult.Payload.PublicKey.Kid, "public key id must match") + s.Equal(model.ExpiresAt.String(), getResult.Payload.ExpiresAt.String(), "expiration date must match") +} + +func (s *HandlerTestSuite) TestGrantCanNotBeCreatedWithSameIssuerSubjectKey() { + createRequestParams := s.newCreateJwtBearerGrantParams( + "ory", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + + _, err := s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().NoError(err, "no errors expected on grant creation") + + _, err = s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().Error(err, "expected error, because grant with same issuer+subject+kid exists") + + kid := uuid.New().String() + createRequestParams.Body.Jwk.Kid = &kid + _, err = s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.NoError(err, "no errors expected on grant creation, because kid is now different") +} + +func (s *HandlerTestSuite) TestGrantCanNotBeCreatedWithMissingFields() { + createRequestParams := s.newCreateJwtBearerGrantParams( + "", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + + _, err := s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().Error(err, "expected error, because grant missing issuer") + + createRequestParams = s.newCreateJwtBearerGrantParams( + "ory", + "", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + + _, err = s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().Error(err, "expected error, because grant missing subject") + + createRequestParams = s.newCreateJwtBearerGrantParams( + "ory", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Time{}, + ) + + _, err = s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Error(err, "expected error, because grant missing expiration date") +} + +func (s *HandlerTestSuite) TestGrantPublicCanBeFetched() { + createRequestParams := s.newCreateJwtBearerGrantParams( + "ory", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + model := createRequestParams.Body + + _, err := s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().NoError(err, "no error expected on grant creation") + + getJWKRequestParams := admin.NewGetJSONWebKeyParams() + getJWKRequestParams.Kid = *model.Jwk.Kid + getJWKRequestParams.Set = *model.Issuer + + getResult, err := s.hydraClient.Admin.GetJSONWebKey(getJWKRequestParams) + + s.Require().NoError(err, "no error expected on fetching public key") + s.Equal(*model.Jwk.Kid, *getResult.Payload.Keys[0].Kid) +} + +func (s *HandlerTestSuite) TestGrantListCanBeFetched() { + createRequestParams := s.newCreateJwtBearerGrantParams( + "ory", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + createRequestParams2 := s.newCreateJwtBearerGrantParams( + "ory2", + "safetyman@example.com", + []string{"profile"}, + time.Now().Add(time.Hour), + ) + + _, err := s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().NoError(err, "no errors expected on grant creation") + + _, err = s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams2) + s.Require().NoError(err, "no errors expected on grant creation") + + getRequestParams := admin.NewListTrustedJwtGrantIssuersParams() + getResult, err := s.hydraClient.Admin.ListTrustedJwtGrantIssuers(getRequestParams) + + s.Require().NoError(err, "no errors expected on grant list fetching") + s.Len(getResult.Payload, 2, "expected to get list of 2 grants") + + getRequestParams.Issuer = createRequestParams2.Body.Issuer + getResult, err = s.hydraClient.Admin.ListTrustedJwtGrantIssuers(getRequestParams) + + s.Require().NoError(err, "no errors expected on grant list fetching") + s.Len(getResult.Payload, 1, "expected to get list of 1 grant, when filtering by issuer") + s.Equal(*createRequestParams2.Body.Issuer, getResult.Payload[0].Issuer, "issuer must match") +} + +func (s *HandlerTestSuite) TestGrantCanBeDeleted() { + createRequestParams := s.newCreateJwtBearerGrantParams( + "ory", + "hackerman@example.com", + []string{"openid", "offline", "profile"}, + time.Now().Add(time.Hour), + ) + + createResult, err := s.hydraClient.Admin.TrustJwtGrantIssuer(createRequestParams) + s.Require().NoError(err, "no errors expected on grant creation") + + deleteRequestParams := admin.NewDeleteTrustedJwtGrantIssuerParams() + deleteRequestParams.ID = createResult.Payload.ID + _, err = s.hydraClient.Admin.DeleteTrustedJwtGrantIssuer(deleteRequestParams) + + s.Require().NoError(err, "no errors expected on grant deletion") + + _, err = s.hydraClient.Admin.DeleteTrustedJwtGrantIssuer(deleteRequestParams) + s.Error(err, "expected error, because grant has been already deleted") +} + +func (s *HandlerTestSuite) generateJWK(publicKey *rsa.PublicKey) *models.JSONWebKey { + jwk := jose.JSONWebKey{ + Key: publicKey, + KeyID: uuid.New().String(), + Algorithm: string(jose.RS256), + Use: "sig", + } + b, err := jwk.MarshalJSON() + s.Require().NoError(err) + + mJWK := &models.JSONWebKey{} + err = mJWK.UnmarshalBinary(b) + s.Require().NoError(err) + + return mJWK +} + +func (s *HandlerTestSuite) newCreateJwtBearerGrantParams( + issuer, subject string, scope []string, expiresAt time.Time, +) *admin.TrustJwtGrantIssuerParams { + createRequestParams := admin.NewTrustJwtGrantIssuerParams() + exp := strfmt.DateTime(expiresAt.UTC().Round(time.Second)) + model := &models.TrustJwtGrantIssuerBody{ + ExpiresAt: &exp, + Issuer: &issuer, + Jwk: s.generateJWK(s.publicKey), + Scope: scope, + Subject: &subject, + } + createRequestParams.SetBody(model) + + return createRequestParams +} + +func (s *HandlerTestSuite) generatePublicKey() *rsa.PublicKey { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + s.Require().NoError(err) + return &privateKey.PublicKey +} diff --git a/oauth2/trust/manager.go b/oauth2/trust/manager.go new file mode 100644 index 00000000000..80473c7f0b1 --- /dev/null +++ b/oauth2/trust/manager.go @@ -0,0 +1,32 @@ +package trust + +import ( + "context" + "time" + + "gopkg.in/square/go-jose.v2" +) + +type GrantManager interface { + CreateGrant(ctx context.Context, g Grant, publicKey jose.JSONWebKey) error + GetConcreteGrant(ctx context.Context, id string) (Grant, error) + DeleteGrant(ctx context.Context, id string) error + GetGrants(ctx context.Context, limit, offset int, optionalIssuer string) ([]Grant, error) + CountGrants(ctx context.Context) (int, error) + FlushInactiveGrants(ctx context.Context, notAfter time.Time, limit int, batchSize int) error +} + +type SQLData struct { + ID string `db:"id"` + Issuer string `db:"issuer"` + Subject string `db:"subject"` + Scope string `db:"scope"` + KeySet string `db:"key_set"` + KeyID string `db:"key_id"` + CreatedAt time.Time `db:"created_at"` + ExpiresAt time.Time `db:"expires_at"` +} + +func (SQLData) TableName() string { + return "hydra_oauth2_trusted_jwt_bearer_issuer" +} diff --git a/oauth2/trust/manager_test_helpers.go b/oauth2/trust/manager_test_helpers.go new file mode 100644 index 00000000000..710deb954f0 --- /dev/null +++ b/oauth2/trust/manager_test_helpers.go @@ -0,0 +1,216 @@ +/* + * Copyright © 2015-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + * + * @author Aeneas Rekkas + * @copyright 2015-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +package trust + +import ( + "context" + "sort" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" + + "github.com/ory/hydra/jwk" +) + +func TestHelperGrantManagerCreateGetDeleteGrant(m GrantManager) func(t *testing.T) { + testGenerator := &jwk.RS256Generator{} + tokenServicePubKey1 := jose.JSONWebKey{} + tokenServicePubKey2 := jose.JSONWebKey{} + mikePubKey := jose.JSONWebKey{} + + return func(t *testing.T) { + keySet, err := testGenerator.Generate("tokenServicePubKey1", "sig") + require.NoError(t, err) + tokenServicePubKey1 = keySet.Keys[1] + + keySet, err = testGenerator.Generate("tokenServicePubKey2", "sig") + require.NoError(t, err) + tokenServicePubKey2 = keySet.Keys[1] + + keySet, err = testGenerator.Generate("mikePubKey", "sig") + require.NoError(t, err) + mikePubKey = keySet.Keys[1] + + storedGrants, err := m.GetGrants(context.TODO(), 100, 0, "") + require.NoError(t, err) + assert.Len(t, storedGrants, 0) + + count, err := m.CountGrants(context.TODO()) + require.NoError(t, err) + assert.Equal(t, 0, count) + + createdAt := time.Now().UTC().Round(time.Second) + expiresAt := createdAt.AddDate(1, 0, 0) + grant := Grant{ + ID: uuid.New().String(), + Issuer: "token-service", + Subject: "bob@example.com", + Scope: []string{"openid", "offline"}, + PublicKey: PublicKey{ + Set: "token-service", + KeyID: "public:tokenServicePubKey1", + }, + CreatedAt: createdAt, + ExpiresAt: expiresAt, + } + err = m.CreateGrant(context.TODO(), grant, tokenServicePubKey1) + require.NoError(t, err) + + storedGrant, err := m.GetConcreteGrant(context.TODO(), grant.ID) + require.NoError(t, err) + assert.Equal(t, grant.ID, storedGrant.ID) + assert.Equal(t, grant.Issuer, storedGrant.Issuer) + assert.Equal(t, grant.Subject, storedGrant.Subject) + assert.Equal(t, grant.Scope, storedGrant.Scope) + assert.Equal(t, grant.PublicKey, storedGrant.PublicKey) + assert.Equal(t, grant.CreatedAt.Format(time.RFC3339), storedGrant.CreatedAt.Format(time.RFC3339)) + assert.Equal(t, grant.ExpiresAt.Format(time.RFC3339), storedGrant.ExpiresAt.Format(time.RFC3339)) + + grant2 := Grant{ + ID: uuid.New().String(), + Issuer: "token-service", + Subject: "maria@example.com", + Scope: []string{"openid"}, + PublicKey: PublicKey{ + Set: "token-service", + KeyID: "public:tokenServicePubKey2", + }, + CreatedAt: createdAt.Add(time.Minute * 5), + ExpiresAt: createdAt.Add(-time.Minute * 5), + } + err = m.CreateGrant(context.TODO(), grant2, tokenServicePubKey2) + require.NoError(t, err) + + grant3 := Grant{ + ID: uuid.New().String(), + Issuer: "https://mike.example.com", + Subject: "mike@example.com", + Scope: []string{"permissions", "openid", "offline"}, + PublicKey: PublicKey{ + Set: "https://mike.example.com", + KeyID: "public:mikePubKey", + }, + CreatedAt: createdAt.Add(time.Hour), + ExpiresAt: createdAt.Add(-time.Hour * 24), + } + err = m.CreateGrant(context.TODO(), grant3, mikePubKey) + require.NoError(t, err) + + count, err = m.CountGrants(context.TODO()) + require.NoError(t, err) + assert.Equal(t, 3, count) + + storedGrants, err = m.GetGrants(context.TODO(), 100, 0, "") + sort.Slice(storedGrants, func(i, j int) bool { + return storedGrants[i].CreatedAt.Before(storedGrants[j].CreatedAt) + }) + require.NoError(t, err) + require.Len(t, storedGrants, 3) + assert.Equal(t, grant.ID, storedGrants[0].ID) + assert.Equal(t, grant2.ID, storedGrants[1].ID) + assert.Equal(t, grant3.ID, storedGrants[2].ID) + + storedGrants, err = m.GetGrants(context.TODO(), 100, 0, "token-service") + sort.Slice(storedGrants, func(i, j int) bool { + return storedGrants[i].CreatedAt.Before(storedGrants[j].CreatedAt) + }) + require.NoError(t, err) + require.Len(t, storedGrants, 2) + assert.Equal(t, grant.ID, storedGrants[0].ID) + assert.Equal(t, grant2.ID, storedGrants[1].ID) + + err = m.DeleteGrant(context.TODO(), grant.ID) + require.NoError(t, err) + + _, err = m.GetConcreteGrant(context.TODO(), grant.ID) + require.Error(t, err) + + count, err = m.CountGrants(context.TODO()) + require.NoError(t, err) + assert.Equal(t, 2, count) + + err = m.FlushInactiveGrants(context.TODO(), grant2.ExpiresAt, 1000, 100) + require.NoError(t, err) + + count, err = m.CountGrants(context.TODO()) + require.NoError(t, err) + assert.Equal(t, 1, count) + + _, err = m.GetConcreteGrant(context.TODO(), grant2.ID) + assert.NoError(t, err) + } +} + +func TestHelperGrantManagerErrors(m GrantManager) func(t *testing.T) { + testGenerator := &jwk.RS256Generator{} + pubKey1 := jose.JSONWebKey{} + pubKey2 := jose.JSONWebKey{} + + return func(t *testing.T) { + keySet, err := testGenerator.Generate("pubKey1", "sig") + require.NoError(t, err) + pubKey1 = keySet.Keys[1] + + keySet, err = testGenerator.Generate("pubKey2", "sig") + require.NoError(t, err) + pubKey2 = keySet.Keys[1] + + createdAt := time.Now() + expiresAt := createdAt.AddDate(1, 0, 0) + grant := Grant{ + ID: uuid.New().String(), + Issuer: "issuer", + Subject: "subject", + Scope: []string{"openid", "offline"}, + PublicKey: PublicKey{ + Set: "set", + KeyID: "public:pubKey1", + }, + CreatedAt: createdAt, + ExpiresAt: expiresAt, + } + err = m.CreateGrant(context.TODO(), grant, pubKey1) + require.NoError(t, err) + + grant.ID = uuid.New().String() + err = m.CreateGrant(context.TODO(), grant, pubKey1) + require.Error(t, err, "error expected, because combination of issuer + subject + key_id must be unique") + + grant2 := grant + grant2.PublicKey = PublicKey{ + Set: "set", + KeyID: "public:pubKey2", + } + err = m.CreateGrant(context.TODO(), grant2, pubKey2) + require.NoError(t, err) + + nonExistingGrantID := uuid.New().String() + err = m.DeleteGrant(context.TODO(), nonExistingGrantID) + require.Error(t, err, "expect error, when deleting non-existing grant") + + _, err = m.GetConcreteGrant(context.TODO(), nonExistingGrantID) + require.Error(t, err, "expect error, when fetching non-existing grant") + } +} diff --git a/oauth2/trust/registry.go b/oauth2/trust/registry.go new file mode 100644 index 00000000000..de7f17238d1 --- /dev/null +++ b/oauth2/trust/registry.go @@ -0,0 +1,16 @@ +package trust + +import ( + "github.com/ory/hydra/x" +) + +type InternalRegistry interface { + x.RegistryWriter + x.RegistryLogger + Registry +} + +type Registry interface { + GrantManager() GrantManager + GrantValidator() *GrantValidator +} diff --git a/oauth2/trust/request.go b/oauth2/trust/request.go new file mode 100644 index 00000000000..3928ccbe627 --- /dev/null +++ b/oauth2/trust/request.go @@ -0,0 +1,24 @@ +package trust + +import ( + "time" + + "gopkg.in/square/go-jose.v2" +) + +type createGrantRequest struct { + // Issuer identifies the principal that issued the JWT assertion (same as iss claim in jwt). + Issuer string `json:"issuer"` + + // Subject identifies the principal that is the subject of the JWT. + Subject string `json:"subject"` + + // Scope contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) + Scope []string `json:"scope"` + + // PublicKeyJWK contains public key in JWK format issued by Issuer, that will be used to check JWT assertion signature. + PublicKeyJWK jose.JSONWebKey `json:"jwk"` + + // ExpiresAt indicates, when grant will expire, so we will reject assertion from Issuer targeting Subject. + ExpiresAt time.Time `json:"expires_at"` +} diff --git a/oauth2/trust/validator.go b/oauth2/trust/validator.go new file mode 100644 index 00000000000..48bd5085fda --- /dev/null +++ b/oauth2/trust/validator.go @@ -0,0 +1,32 @@ +package trust + +import ( + "github.com/ory/x/errorsx" +) + +type GrantValidator struct { +} + +func NewGrantValidator() *GrantValidator { + return &GrantValidator{} +} + +func (v *GrantValidator) Validate(request createGrantRequest) error { + if request.Issuer == "" { + return errorsx.WithStack(ErrMissingRequiredParameter.WithHint("Field 'issuer' is required.")) + } + + if request.Subject == "" { + return errorsx.WithStack(ErrMissingRequiredParameter.WithHint("Field 'subject' is required.")) + } + + if request.ExpiresAt.IsZero() { + return errorsx.WithStack(ErrMissingRequiredParameter.WithHint("Field 'expires_at' is required.")) + } + + if request.PublicKeyJWK.KeyID == "" { + return errorsx.WithStack(ErrMissingRequiredParameter.WithHint("Field 'jwk' must contain JWK with kid header.")) + } + + return nil +} diff --git a/oauth2/trust/validator_test.go b/oauth2/trust/validator_test.go new file mode 100644 index 00000000000..ec850e61606 --- /dev/null +++ b/oauth2/trust/validator_test.go @@ -0,0 +1,93 @@ +package trust + +import ( + "testing" + "time" + + "gopkg.in/square/go-jose.v2" +) + +func TestEmptyIssuerIsInvalid(t *testing.T) { + v := GrantValidator{} + + r := createGrantRequest{ + Issuer: "", + Subject: "valid-subject", + ExpiresAt: time.Now().Add(time.Hour * 10), + PublicKeyJWK: jose.JSONWebKey{ + KeyID: "valid-key-id", + }, + } + + if err := v.Validate(r); err == nil { + t.Error("an empty issuer should not be valid") + } +} + +func TestEmptySubjectIsInvalid(t *testing.T) { + v := GrantValidator{} + + r := createGrantRequest{ + Issuer: "valid-issuer", + Subject: "", + ExpiresAt: time.Now().Add(time.Hour * 10), + PublicKeyJWK: jose.JSONWebKey{ + KeyID: "valid-key-id", + }, + } + + if err := v.Validate(r); err == nil { + t.Error("an empty subject should not be valid") + } +} + +func TestEmptyExpiresAtIsInvalid(t *testing.T) { + v := GrantValidator{} + + r := createGrantRequest{ + Issuer: "valid-issuer", + Subject: "valid-subject", + ExpiresAt: time.Time{}, + PublicKeyJWK: jose.JSONWebKey{ + KeyID: "valid-key-id", + }, + } + + if err := v.Validate(r); err == nil { + t.Error("an empty expiration should not be valid") + } +} + +func TestEmptyPublicKeyIdIsInvalid(t *testing.T) { + v := GrantValidator{} + + r := createGrantRequest{ + Issuer: "valid-issuer", + Subject: "valid-subject", + ExpiresAt: time.Now().Add(time.Hour * 10), + PublicKeyJWK: jose.JSONWebKey{ + KeyID: "", + }, + } + + if err := v.Validate(r); err == nil { + t.Error("an empty public key id should not be valid") + } +} + +func TestIsValid(t *testing.T) { + v := GrantValidator{} + + r := createGrantRequest{ + Issuer: "valid-issuer", + Subject: "valid-subject", + ExpiresAt: time.Now().Add(time.Hour * 10), + PublicKeyJWK: jose.JSONWebKey{ + KeyID: "valid-key-id", + }, + } + + if err := v.Validate(r); err != nil { + t.Error("A request with an issuer, a subject, an expiration and a public key should be valid") + } +} diff --git a/package-lock.json b/package-lock.json index 32dac4d5b98..be95a68eb77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,53 +8,14 @@ "name": "@oryd/hydra", "version": "0.0.0", "devDependencies": { - "cypress": "^6.6.0", + "cypress": "^7.7.0", + "dayjs": "^1.10.6", "ory-prettier-styles": "1.1.1", "prettier": "2.1.2", "standard": "^12.0.1", "wait-on": "^3.2.0" } }, - "node_modules/@cypress/listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", - "dev": true, - "dependencies": { - "chalk": "^1.1.3", - "cli-cursor": "^1.0.2", - "date-fns": "^1.27.2", - "figures": "^1.7.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@cypress/listr-verbose-renderer/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@cypress/listr-verbose-renderer/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@cypress/request": { "version": "2.88.5", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz", @@ -109,30 +70,10 @@ "lodash.once": "^4.1.1" } }, - "node_modules/@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", - "dev": true, - "dependencies": { - "any-observable": "^0.3.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - }, - "zen-observable": { - "optional": true - } - } - }, "node_modules/@types/node": { - "version": "12.12.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.50.tgz", - "integrity": "sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==", + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==", "dev": true }, "node_modules/@types/sinonjs__fake-timers": { @@ -147,6 +88,16 @@ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", + "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/acorn": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", @@ -165,6 +116,19 @@ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", "dev": true }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -183,6 +147,15 @@ "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", "dev": true }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -210,15 +183,6 @@ "node": ">=0.10.0" } }, - "node_modules/any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -279,6 +243,15 @@ "node": ">=0.8" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", @@ -397,12 +370,6 @@ "node": "*" } }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -481,9 +448,9 @@ } }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", "dev": true }, "node_modules/circular-json": { @@ -492,16 +459,25 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "dependencies": { - "restore-cursor": "^1.0.1" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/cli-table3": { @@ -521,42 +497,19 @@ } }, "node_modules/cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", - "dev": true, - "dependencies": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "dependencies": { - "number-is-nan": "^1.0.0" + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "node": ">=8" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-width": { @@ -565,15 +518,6 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -589,6 +533,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "node_modules/colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -635,21 +585,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "node_modules/contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -688,48 +623,49 @@ } }, "node_modules/cypress": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.6.0.tgz", - "integrity": "sha512-+Xx3Zn653LJHUsCb9h1Keql2jlazbr1ROmbY6DFJMmXKLgXP4ez9cE403W93JNGRbZK0Tng3R/oP8mvd9XAPVg==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-7.7.0.tgz", + "integrity": "sha512-uYBYXNoI5ym0UxROwhQXWTi8JbUEjpC6l/bzoGZNxoKGsLrC1SDPgIDJMgLX/MeEdPL0UInXLDUWN/rSyZUCjQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/listr-verbose-renderer": "^0.4.1", "@cypress/request": "^2.88.5", "@cypress/xvfb": "^1.2.4", - "@types/node": "12.12.50", - "@types/sinonjs__fake-timers": "^6.0.1", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "^6.0.2", "@types/sizzle": "^2.3.2", - "arch": "^2.1.2", - "blob-util": "2.0.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", "bluebird": "^3.7.2", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", "cli-table3": "~0.6.0", "commander": "^5.1.0", "common-tags": "^1.8.0", - "dayjs": "^1.9.3", - "debug": "4.3.2", - "eventemitter2": "^6.4.2", - "execa": "^4.0.2", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "^1.7.0", - "fs-extra": "^9.0.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", - "listr": "^0.14.3", - "lodash": "^4.17.19", + "listr2": "^3.8.3", + "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.5", - "moment": "^2.29.1", "ospath": "^1.2.2", - "pretty-bytes": "^5.4.1", + "pretty-bytes": "^5.6.0", "ramda": "~0.27.1", "request-progress": "^3.0.0", - "supports-color": "^7.2.0", + "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", "url": "^0.11.0", @@ -739,7 +675,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" } }, "node_modules/cypress/node_modules/ansi-styles": { @@ -773,6 +709,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cypress/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -817,12 +765,6 @@ "node": ">=8" } }, - "node_modules/cypress/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/cypress/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -839,15 +781,18 @@ } }, "node_modules/cypress/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/cypress/node_modules/tmp": { @@ -874,16 +819,10 @@ "node": ">=0.10" } }, - "node_modules/date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, "node_modules/dayjs": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", - "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==", "dev": true }, "node_modules/debug": { @@ -973,15 +912,6 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -997,6 +927,18 @@ "once": "^1.4.0" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1449,30 +1391,6 @@ "node": ">= 8" } }, - "node_modules/execa/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/execa/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1530,15 +1448,6 @@ "node": ">=4" } }, - "node_modules/exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1560,35 +1469,42 @@ } }, "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", + "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "dependencies": { - "ms": "2.0.0" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1616,17 +1532,28 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "dependencies": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" + "escape-string-regexp": "^1.0.5" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/file-entry-cache": { @@ -1791,15 +1718,15 @@ } }, "node_modules/global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", "dev": true, "dependencies": { - "ini": "1.3.7" + "ini": "2.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1954,12 +1881,12 @@ } }, "node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/inflight": { @@ -1979,10 +1906,13 @@ "dev": true }, "node_modules/ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } }, "node_modules/inquirer": { "version": "5.2.0", @@ -2116,12 +2046,12 @@ } }, "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", "dev": true, "dependencies": { - "ci-info": "^2.0.0" + "ci-info": "^3.1.1" }, "bin": { "is-ci": "bin.js" @@ -2146,42 +2076,21 @@ } }, "node_modules/is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, "dependencies": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "dependencies": { - "symbol-observable": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/is-observable/node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -2402,171 +2311,31 @@ "node": ">= 0.8.0" } }, - "node_modules/listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "dependencies": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "node_modules/listr2": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.10.0.tgz", + "integrity": "sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==", "dev": true, "dependencies": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" + "cli-truncate": "^2.1.0", + "colorette": "^1.2.2", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rxjs": "^6.6.7", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=10.0.0" }, "peerDependencies": { - "listr": "^0.14.2" - } - }, - "node_modules/listr-update-renderer/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "dependencies": { - "chalk": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "enquirer": ">= 2.3.0 < 3" } }, - "node_modules/listr/node_modules/rxjs": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", - "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", + "node_modules/listr2/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, "dependencies": { "tslib": "^1.9.0" @@ -2707,54 +2476,121 @@ } }, "node_modules/log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, "dependencies": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "dependencies": { - "restore-cursor": "^2.0.0" + "type-fest": "^0.21.3" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/onetime": { + "node_modules/log-update/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "mimic-fn": "^1.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=4" + "node": ">=7.0.0" } }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/loose-envify": { @@ -2835,19 +2671,10 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/mute-stream": { @@ -2901,15 +2728,6 @@ "node": ">=8" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -2947,12 +2765,27 @@ } }, "node_modules/onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" } }, "node_modules/optionator": { @@ -3018,12 +2851,18 @@ } }, "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -3189,12 +3028,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -3336,21 +3169,6 @@ "node": ">=4" } }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", @@ -3432,16 +3250,16 @@ } }, "node_modules/restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "dependencies": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/rimraf": { @@ -3541,14 +3359,52 @@ "dev": true }, "node_modules/slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -3642,15 +3498,6 @@ "pkg-conf": "^2.0.0" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -3918,11 +3765,17 @@ "node": ">= 0.8.0" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/uniq": { "version": "1.0.1", @@ -3973,12 +3826,6 @@ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", "dev": true }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "node_modules/uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -4050,59 +3897,74 @@ "dev": true }, "node_modules/wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=4" + "node": ">=7.0.0" } }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "dependencies": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/wrappy": { @@ -4141,51 +4003,9 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } - }, - "node_modules/yauzl/node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } } }, "dependencies": { - "@cypress/listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-cursor": "^1.0.2", - "date-fns": "^1.27.2", - "figures": "^1.7.0" - }, - "dependencies": { - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, "@cypress/request": { "version": "2.88.5", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz", @@ -4236,19 +4056,10 @@ "lodash.once": "^4.1.1" } }, - "@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", - "dev": true, - "requires": { - "any-observable": "^0.3.0" - } - }, "@types/node": { - "version": "12.12.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.50.tgz", - "integrity": "sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==", + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==", "dev": true }, "@types/sinonjs__fake-timers": { @@ -4263,6 +4074,16 @@ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "dev": true }, + "@types/yauzl": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", + "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, "acorn": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", @@ -4275,6 +4096,16 @@ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", "dev": true }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -4293,6 +4124,12 @@ "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", "dev": true }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -4311,12 +4148,6 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, - "any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true - }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -4357,6 +4188,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", @@ -4462,12 +4299,6 @@ "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, "cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -4530,9 +4361,9 @@ "dev": true }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", "dev": true }, "circular-json": { @@ -4541,13 +4372,19 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "restore-cursor": "^1.0.1" + "restore-cursor": "^3.1.0" } }, "cli-table3": { @@ -4562,35 +4399,13 @@ } }, "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "requires": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" } }, "cli-width": { @@ -4599,12 +4414,6 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4620,6 +4429,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -4654,18 +4469,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -4698,47 +4501,48 @@ } }, "cypress": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.6.0.tgz", - "integrity": "sha512-+Xx3Zn653LJHUsCb9h1Keql2jlazbr1ROmbY6DFJMmXKLgXP4ez9cE403W93JNGRbZK0Tng3R/oP8mvd9XAPVg==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-7.7.0.tgz", + "integrity": "sha512-uYBYXNoI5ym0UxROwhQXWTi8JbUEjpC6l/bzoGZNxoKGsLrC1SDPgIDJMgLX/MeEdPL0UInXLDUWN/rSyZUCjQ==", "dev": true, "requires": { - "@cypress/listr-verbose-renderer": "^0.4.1", "@cypress/request": "^2.88.5", "@cypress/xvfb": "^1.2.4", - "@types/node": "12.12.50", - "@types/sinonjs__fake-timers": "^6.0.1", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "^6.0.2", "@types/sizzle": "^2.3.2", - "arch": "^2.1.2", - "blob-util": "2.0.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", "bluebird": "^3.7.2", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", "cli-table3": "~0.6.0", "commander": "^5.1.0", "common-tags": "^1.8.0", - "dayjs": "^1.9.3", - "debug": "4.3.2", - "eventemitter2": "^6.4.2", - "execa": "^4.0.2", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "^1.7.0", - "fs-extra": "^9.0.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", - "listr": "^0.14.3", - "lodash": "^4.17.19", + "listr2": "^3.8.3", + "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.5", - "moment": "^2.29.1", "ospath": "^1.2.2", - "pretty-bytes": "^5.4.1", + "pretty-bytes": "^5.6.0", "ramda": "~0.27.1", "request-progress": "^3.0.0", - "supports-color": "^7.2.0", + "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", "url": "^0.11.0", @@ -4762,6 +4566,17 @@ "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "color-convert": { @@ -4794,12 +4609,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4810,9 +4619,9 @@ } }, "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -4838,16 +4647,10 @@ "assert-plus": "^1.0.0" } }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, "dayjs": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", - "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==", "dev": true }, "debug": { @@ -4927,12 +4730,6 @@ "safer-buffer": "^2.1.0" } }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4948,6 +4745,15 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5325,21 +5131,6 @@ "which": "^2.0.1" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5381,12 +5172,6 @@ "pify": "^2.2.0" } }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5405,31 +5190,25 @@ } }, "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true } } }, @@ -5457,14 +5236,22 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" + "escape-string-regexp": "^1.0.5" } }, "file-entry-cache": { @@ -5599,12 +5386,12 @@ } }, "global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", "dev": true, "requires": { - "ini": "1.3.7" + "ini": "2.0.0" } }, "globals": { @@ -5716,9 +5503,9 @@ "dev": true }, "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, "inflight": { @@ -5738,9 +5525,9 @@ "dev": true }, "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, "inquirer": { @@ -5847,12 +5634,12 @@ "dev": true }, "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", "dev": true, "requires": { - "ci-info": "^2.0.0" + "ci-info": "^3.1.1" } }, "is-date-object": { @@ -5868,30 +5655,13 @@ "dev": true }, "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, "requires": { - "symbol-observable": "^1.1.0" - }, - "dependencies": { - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true - } + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" } }, "is-path-inside": { @@ -6053,165 +5823,54 @@ "json-schema": "0.2.3", "verror": "1.10.0" } - }, - "jsx-ast-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", - "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", - "dev": true, - "requires": { - "array-includes": "^3.0.3" - } - }, - "lazy-ass": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", - "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "requires": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "rxjs": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", - "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - } - } - }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true - }, - "listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + }, + "jsx-ast-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", + "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", + "dev": true, + "requires": { + "array-includes": "^3.0.3" + } + }, + "lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, - "listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "listr2": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.10.0.tgz", + "integrity": "sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==", "dev": true, "requires": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" + "cli-truncate": "^2.1.0", + "colorette": "^1.2.2", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rxjs": "^6.6.7", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" }, "dependencies": { - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "tslib": "^1.9.0" } } } @@ -6319,42 +5978,85 @@ } }, "log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "dependencies": { - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "type-fest": "^0.21.3" } }, - "onetime": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "color-name": "~1.1.4" } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } } } @@ -6419,16 +6121,10 @@ "minimist": "^1.2.5" } }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", - "dev": true - }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "mute-stream": { @@ -6478,12 +6174,6 @@ } } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -6512,10 +6202,21 @@ } }, "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } }, "optionator": { "version": "0.8.2", @@ -6568,10 +6269,13 @@ } }, "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } }, "p-try": { "version": "1.0.0", @@ -6691,12 +6395,6 @@ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -6813,21 +6511,6 @@ } } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", @@ -6897,13 +6580,13 @@ "dev": true }, "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" } }, "rimraf": { @@ -6985,10 +6668,41 @@ "dev": true }, "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } }, "spdx-correct": { "version": "3.1.0", @@ -7074,15 +6788,6 @@ "pkg-conf": "^2.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -7301,10 +7006,10 @@ "prelude-ls": "~1.1.2" } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, "uniq": { @@ -7352,12 +7057,6 @@ } } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -7414,44 +7113,53 @@ "dev": true }, "wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "color-name": "~1.1.4" } }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" } } } @@ -7485,17 +7193,6 @@ "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" - }, - "dependencies": { - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - } } } } diff --git a/package.json b/package.json index 14443d70e76..33d9a5db7c8 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "lint": "standard --fix \"test/**/*.js\" \"cypress/**/*.js\"" }, "devDependencies": { - "cypress": "^6.6.0", + "cypress": "^7.7.0", + "dayjs": "^1.10.6", + "jsonwebtoken": "^8.5.1", "ory-prettier-styles": "1.1.1", "prettier": "2.1.2", "standard": "^12.0.1", diff --git a/persistence/definitions.go b/persistence/definitions.go index c3c6cfa84da..4afdaa63207 100644 --- a/persistence/definitions.go +++ b/persistence/definitions.go @@ -6,6 +6,7 @@ import ( "github.com/ory/hydra/client" "github.com/ory/hydra/consent" "github.com/ory/hydra/jwk" + "github.com/ory/hydra/oauth2/trust" "github.com/ory/hydra/x" "github.com/ory/x/popx" @@ -18,6 +19,7 @@ type ( client.Manager x.FositeStorer jwk.Manager + trust.GrantManager MigrationStatus(ctx context.Context) (popx.MigrationStatuses, error) MigrateDown(context.Context, int) error diff --git a/persistence/sql/migratest/testdata/20211226155900_testdata.sql b/persistence/sql/migratest/testdata/20211226155900_testdata.sql new file mode 100644 index 00000000000..0d5ce9ef6ea --- /dev/null +++ b/persistence/sql/migratest/testdata/20211226155900_testdata.sql @@ -0,0 +1,4 @@ +INSERT INTO hydra_jwk (pk, sid, kid, version, keydata, created_at) VALUES (8, 'sid-0008', 'kid-0008', 2, 'key-0002', now()); + +INSERT INTO hydra_oauth2_trusted_jwt_bearer_issuer (id, issuer, subject, scope, key_set, key_id) +VALUES ('30e51720-4a88-48ca-8243-de7d8f461674', 'some-issuer', 'some-subject', 'some-scope', 'sid-0008', 'kid-0008'); diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.cockroach.down.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.cockroach.down.sql new file mode 100644 index 00000000000..6757c1859ee --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.cockroach.down.sql @@ -0,0 +1,3 @@ +DROP INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx; + +DROP TABLE IF EXISTS hydra_oauth2_trusted_jwt_bearer_issuer; diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.cockroach.up.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.cockroach.up.sql new file mode 100644 index 00000000000..b2291acafa0 --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_trusted_jwt_bearer_issuer +( + id UUID PRIMARY KEY, + issuer VARCHAR(255) NOT NULL, + subject VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + key_set varchar(255) NOT NULL, + key_id varchar(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() NOT NULL, + expires_at TIMESTAMP DEFAULT NOW() NOT NULL, + UNIQUE (issuer, subject, key_id), + FOREIGN KEY (key_set, key_id) REFERENCES hydra_jwk (sid, kid) ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx ON hydra_oauth2_trusted_jwt_bearer_issuer (expires_at); diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.mysql.down.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.mysql.down.sql new file mode 100644 index 00000000000..6757c1859ee --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.mysql.down.sql @@ -0,0 +1,3 @@ +DROP INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx; + +DROP TABLE IF EXISTS hydra_oauth2_trusted_jwt_bearer_issuer; diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.mysql.up.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.mysql.up.sql new file mode 100644 index 00000000000..7d53d510659 --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_trusted_jwt_bearer_issuer +( + id VARCHAR(36) PRIMARY KEY, + issuer VARCHAR(255) NOT NULL, + subject VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + key_set varchar(255) NOT NULL, + key_id varchar(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + expires_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + UNIQUE (issuer, subject, key_id), + FOREIGN KEY (key_set, key_id) REFERENCES hydra_jwk (sid, kid) ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx ON hydra_oauth2_trusted_jwt_bearer_issuer (expires_at); diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.postgres.down.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.postgres.down.sql new file mode 100644 index 00000000000..6757c1859ee --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.postgres.down.sql @@ -0,0 +1,3 @@ +DROP INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx; + +DROP TABLE IF EXISTS hydra_oauth2_trusted_jwt_bearer_issuer; diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.postgres.up.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.postgres.up.sql new file mode 100644 index 00000000000..b2291acafa0 --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_trusted_jwt_bearer_issuer +( + id UUID PRIMARY KEY, + issuer VARCHAR(255) NOT NULL, + subject VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + key_set varchar(255) NOT NULL, + key_id varchar(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() NOT NULL, + expires_at TIMESTAMP DEFAULT NOW() NOT NULL, + UNIQUE (issuer, subject, key_id), + FOREIGN KEY (key_set, key_id) REFERENCES hydra_jwk (sid, kid) ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx ON hydra_oauth2_trusted_jwt_bearer_issuer (expires_at); diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.sqlite.down.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.sqlite.down.sql new file mode 100644 index 00000000000..6757c1859ee --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.sqlite.down.sql @@ -0,0 +1,3 @@ +DROP INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx; + +DROP TABLE IF EXISTS hydra_oauth2_trusted_jwt_bearer_issuer; diff --git a/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.sqlite.up.sql b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.sqlite.up.sql new file mode 100644 index 00000000000..0e175fc5dfe --- /dev/null +++ b/persistence/sql/migrations/20211226155900000000_grant_jwk_bearer.sqlite.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_trusted_jwt_bearer_issuer +( + id VARCHAR(36) PRIMARY KEY, + issuer VARCHAR(255) NOT NULL, + subject VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + key_set varchar(255) NOT NULL, + key_id varchar(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + expires_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + UNIQUE (issuer, subject, key_id), + FOREIGN KEY (key_set, key_id) REFERENCES hydra_jwk (sid, kid) ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_trusted_jwt_bearer_issuer_expires_at_idx ON hydra_oauth2_trusted_jwt_bearer_issuer (expires_at); diff --git a/persistence/sql/persister_grant_jwk.go b/persistence/sql/persister_grant_jwk.go new file mode 100644 index 00000000000..4601b38c44d --- /dev/null +++ b/persistence/sql/persister_grant_jwk.go @@ -0,0 +1,200 @@ +package sql + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/gobuffalo/pop/v5" + "gopkg.in/square/go-jose.v2" + + "github.com/ory/hydra/oauth2/trust" + "github.com/ory/x/errorsx" + "github.com/ory/x/stringsx" + + "github.com/ory/x/sqlcon" +) + +var _ trust.GrantManager = &Persister{} + +func (p *Persister) CreateGrant(ctx context.Context, g trust.Grant, publicKey jose.JSONWebKey) error { + return p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + // add key, if it doesn't exist + if _, err := p.GetKey(ctx, g.PublicKey.Set, g.PublicKey.KeyID); err != nil { + if errorsx.Cause(err) != sqlcon.ErrNoRows { + return sqlcon.HandleError(err) + } + + if err = p.AddKey(ctx, g.PublicKey.Set, &publicKey); err != nil { + return sqlcon.HandleError(err) + } + } + + data := p.sqlDataFromJWTGrant(g) + + return sqlcon.HandleError(p.Connection(ctx).Create(&data)) + }) +} + +func (p *Persister) GetConcreteGrant(ctx context.Context, id string) (trust.Grant, error) { + var data trust.SQLData + if err := p.Connection(ctx).Where("id = ?", id).First(&data); err != nil { + return trust.Grant{}, sqlcon.HandleError(err) + } + + return p.jwtGrantFromSQlData(data), nil +} + +func (p *Persister) DeleteGrant(ctx context.Context, id string) error { + return p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + grant, err := p.GetConcreteGrant(ctx, id) + if err != nil { + return sqlcon.HandleError(err) + } + + if err := p.Connection(ctx).Destroy(&trust.SQLData{ID: grant.ID}); err != nil { + return sqlcon.HandleError(err) + } + + return p.DeleteKey(ctx, grant.PublicKey.Set, grant.PublicKey.KeyID) + }) +} + +func (p *Persister) GetGrants(ctx context.Context, limit, offset int, optionalIssuer string) ([]trust.Grant, error) { + grantsData := make([]trust.SQLData, 0) + + query := p.Connection(ctx).Paginate(offset/limit+1, limit).Order("id") + if optionalIssuer != "" { + query = query.Where("issuer = ?", optionalIssuer) + } + + if err := query.All(&grantsData); err != nil { + return nil, sqlcon.HandleError(err) + } + + grants := make([]trust.Grant, 0, len(grantsData)) + for _, data := range grantsData { + grants = append(grants, p.jwtGrantFromSQlData(data)) + } + + return grants, nil +} + +func (p *Persister) CountGrants(ctx context.Context) (int, error) { + n, err := p.Connection(ctx).Count(&trust.SQLData{}) + return n, sqlcon.HandleError(err) +} + +func (p *Persister) GetPublicKey(ctx context.Context, issuer string, subject string, keyId string) (*jose.JSONWebKey, error) { + var data trust.SQLData + query := p.Connection(ctx). + Where("issuer = ?", issuer). + Where("subject = ?", subject). + Where("key_id = ?", keyId) + if err := query.First(&data); err != nil { + return nil, sqlcon.HandleError(err) + } + + keySet, err := p.GetKey(ctx, data.KeySet, keyId) + if err != nil { + return nil, err + } + + return &keySet.Keys[0], nil +} + +func (p *Persister) GetPublicKeys(ctx context.Context, issuer string, subject string) (*jose.JSONWebKeySet, error) { + grantsData := make([]trust.SQLData, 0) + query := p.Connection(ctx). + Where("issuer = ?", issuer). + Where("subject = ?", subject) + if err := query.All(&grantsData); err != nil { + return nil, sqlcon.HandleError(err) + } + + if len(grantsData) == 0 { + return &jose.JSONWebKeySet{}, nil + } + + // because keys must be grouped by issuer, we can retrieve set name from first grant + keySet, err := p.GetKeySet(ctx, grantsData[0].KeySet) + if err != nil { + return nil, err + } + + // find keys, that belong to grants + filteredKeySet := &jose.JSONWebKeySet{} + for _, data := range grantsData { + if keys := keySet.Key(data.KeyID); len(keys) > 0 { + filteredKeySet.Keys = append(filteredKeySet.Keys, keys...) + } + } + + return filteredKeySet, nil +} + +func (p *Persister) GetPublicKeyScopes(ctx context.Context, issuer string, subject string, keyId string) ([]string, error) { + var data trust.SQLData + query := p.Connection(ctx). + Where("issuer = ?", issuer). + Where("subject = ?", subject). + Where("key_id = ?", keyId) + if err := query.First(&data); err != nil { + return nil, sqlcon.HandleError(err) + } + + return p.jwtGrantFromSQlData(data).Scope, nil +} + +func (p *Persister) IsJWTUsed(ctx context.Context, jti string) (bool, error) { + err := p.ClientAssertionJWTValid(ctx, jti) + if err != nil { + return true, nil + } + + return false, nil +} + +func (p *Persister) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) error { + return p.SetClientAssertionJWT(ctx, jti, exp) +} + +func (p *Persister) sqlDataFromJWTGrant(g trust.Grant) trust.SQLData { + return trust.SQLData{ + ID: g.ID, + Issuer: g.Issuer, + Subject: g.Subject, + Scope: strings.Join(g.Scope, "|"), + KeySet: g.PublicKey.Set, + KeyID: g.PublicKey.KeyID, + CreatedAt: g.CreatedAt, + ExpiresAt: g.ExpiresAt, + } +} + +func (p *Persister) jwtGrantFromSQlData(data trust.SQLData) trust.Grant { + return trust.Grant{ + ID: data.ID, + Issuer: data.Issuer, + Subject: data.Subject, + Scope: stringsx.Splitx(data.Scope, "|"), + PublicKey: trust.PublicKey{ + Set: data.KeySet, + KeyID: data.KeyID, + }, + CreatedAt: data.CreatedAt, + ExpiresAt: data.ExpiresAt, + } +} + +func (p *Persister) FlushInactiveGrants(ctx context.Context, notAfter time.Time, limit int, batchSize int) error { + deleteUntil := time.Now().UTC() + if deleteUntil.After(notAfter) { + deleteUntil = notAfter + } + return sqlcon.HandleError(p.Connection(ctx).RawQuery( + fmt.Sprintf("DELETE FROM %s WHERE expires_at < ?", trust.SQLData{}.TableName()), + deleteUntil, + ).Exec()) +} diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index 74c72e219c0..4ffa1f516e3 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -4,6 +4,9 @@ import ( "testing" "github.com/pborman/uuid" + + "github.com/ory/hydra/oauth2/trust" + "github.com/stretchr/testify/require" "github.com/ory/hydra/internal/testhelpers" @@ -60,5 +63,10 @@ func TestManagers(t *testing.T) { }) } }) + + t.Run("package=grant/trust/manager="+k, func(t *testing.T) { + t.Run("case=create-get-delete", trust.TestHelperGrantManagerCreateGetDeleteGrant(m.GrantManager())) + t.Run("case=errors", trust.TestHelperGrantManagerErrors(m.GrantManager())) + }) } } diff --git a/spec/api.json b/spec/api.json index 65bb566ada6..07259b8f97b 100755 --- a/spec/api.json +++ b/spec/api.json @@ -397,6 +397,43 @@ } } }, + "/grants/jwt-bearer/flush": { + "post": { + "description": "This endpoint flushes expired jwt-bearer grants from the database. You can set a time after which no tokens will be\nnot be touched, in case you want to keep recent tokens for auditing. Refresh tokens can not be flushed as they are deleted\nautomatically when performing the refresh flow.", + "consumes": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "admin" + ], + "summary": "Flush Expired jwt-bearer grants.", + "operationId": "flushInactiveJwtBearerGrants", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/flushInactiveJwtBearerGrantsParams" + } + } + ], + "responses": { + "204": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201." + }, + "500": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + } + } + } + }, "/health/alive": { "get": { "description": "This endpoint returns a 200 status code when the HTTP server is up running.\nThis status does currently not include checks whether the database connection is working.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", @@ -1854,6 +1891,208 @@ } } }, + "/trust/grants/jwt-bearer/issuers": { + "get": { + "description": "Use this endpoint to list all trusted JWT Bearer Grant Type Issuers.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "admin" + ], + "summary": "List Trusted OAuth2 JWT Bearer Grant Type Issuers", + "operationId": "listTrustedJwtGrantIssuers", + "parameters": [ + { + "type": "string", + "description": "If optional \"issuer\" is supplied, only jwt-bearer grants with this issuer will be returned.", + "name": "issuer", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "The maximum amount of policies returned, upper bound is 500 policies", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "The offset from where to start looking.", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "trustedJwtGrantIssuers", + "schema": { + "$ref": "#/definitions/trustedJwtGrantIssuers" + } + }, + "500": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + } + } + }, + "post": { + "description": "Use this endpoint to establish a trust relationship for a JWT issuer\nto perform JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication\nand Authorization Grants [RFC7523](https://datatracker.ietf.org/doc/html/rfc7523).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "admin" + ], + "summary": "Trust an OAuth2 JWT Bearer Grant Type Issuer", + "operationId": "trustJwtGrantIssuer", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/trustJwtGrantIssuerBody" + } + } + ], + "responses": { + "201": { + "description": "trustedJwtGrantIssuer", + "schema": { + "$ref": "#/definitions/trustedJwtGrantIssuer" + } + }, + "400": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + }, + "409": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + }, + "500": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + } + } + } + }, + "/trust/grants/jwt-bearer/issuers/{id}": { + "get": { + "description": "Use this endpoint to get a trusted JWT Bearer Grant Type Issuer. The ID is the one returned when you\ncreated the trust relationship.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "admin" + ], + "summary": "Get a Trusted OAuth2 JWT Bearer Grant Type Issuer", + "operationId": "getTrustedJwtGrantIssuer", + "parameters": [ + { + "type": "string", + "description": "The id of the desired grant", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "trustedJwtGrantIssuer", + "schema": { + "$ref": "#/definitions/trustedJwtGrantIssuer" + } + }, + "404": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + }, + "500": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + } + } + }, + "delete": { + "description": "Use this endpoint to delete trusted JWT Bearer Grant Type Issuer. The ID is the one returned when you\ncreated the trust relationship.\n\nOnce deleted, the associated issuer will no longer be able to perform the JSON Web Token (JWT) Profile\nfor OAuth 2.0 Client Authentication and Authorization Grant.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "admin" + ], + "summary": "Delete a Trusted OAuth2 JWT Bearer Grant Type Issuer", + "operationId": "deleteTrustedJwtGrantIssuer", + "parameters": [ + { + "type": "string", + "description": "The id of the desired grant", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201." + }, + "404": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + }, + "500": { + "description": "genericError", + "schema": { + "$ref": "#/definitions/genericError" + } + } + } + } + }, "/userinfo": { "get": { "security": [ @@ -2512,7 +2751,7 @@ } }, "Scope": { - "description": "The level at which the volume exists. Either `global` for cluster-wide,\nor `local` for machine level.", + "description": "The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level.", "type": "string" }, "Status": { @@ -2692,6 +2931,16 @@ } } }, + "flushInactiveJwtBearerGrantsParams": { + "type": "object", + "properties": { + "notAfter": { + "description": "The \"notAfter\" sets after which point grants should not be flushed. This is useful when you want to keep a history\nof recently added grants.", + "type": "string", + "format": "date-time" + } + } + }, "flushInactiveOAuth2TokensRequest": { "type": "object", "properties": { @@ -2702,6 +2951,50 @@ } } }, + "genericError": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "code": { + "description": "The status code", + "type": "integer", + "format": "int64", + "example": 404 + }, + "debug": { + "description": "Debug information\n\nThis field is often not exposed to protect against leaking\nsensitive information.", + "type": "string", + "example": "SQL field \"foo\" is not a bool." + }, + "details": { + "description": "Further error details", + "type": "object", + "additionalProperties": true + }, + "message": { + "description": "Error message\n\nThe error's message.", + "type": "string", + "example": "The resource could not be found" + }, + "reason": { + "description": "A human-readable reason for the error", + "type": "string", + "example": "User with ID 1234 does not exist." + }, + "request": { + "description": "The request ID\n\nThe request ID is often exposed internally in order to trace\nerrors across service architectures. This is often a UUID.", + "type": "string", + "example": "d7ef54b1-ec15-46e6-bccb-524b82c035e6" + }, + "status": { + "description": "The status description", + "type": "string", + "example": "Not Found" + } + } + }, "healthNotReadyStatus": { "type": "object", "properties": { @@ -3192,6 +3485,111 @@ } } }, + "trustJwtGrantIssuerBody": { + "type": "object", + "required": [ + "issuer", + "subject", + "scope", + "jwk", + "expires_at" + ], + "properties": { + "expires_at": { + "description": "The \"expires_at\" indicates, when grant will expire, so we will reject assertion from \"issuer\" targeting \"subject\".", + "type": "string", + "format": "date-time" + }, + "issuer": { + "description": "The \"issuer\" identifies the principal that issued the JWT assertion (same as \"iss\" claim in JWT).", + "type": "string", + "example": "https://jwt-idp.example.com" + }, + "jwk": { + "$ref": "#/definitions/JSONWebKey" + }, + "scope": { + "description": "The \"scope\" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749])", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "openid", + "offline" + ] + }, + "subject": { + "description": "The \"subject\" identifies the principal that is the subject of the JWT.", + "type": "string", + "example": "mike@example.com" + } + } + }, + "trustedJsonWebKey": { + "type": "object", + "properties": { + "kid": { + "description": "The \"key_id\" is key unique identifier (same as kid header in jws/jwt).", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426655440000" + }, + "set": { + "description": "The \"set\" is basically a name for a group(set) of keys. Will be the same as \"issuer\" in grant.", + "type": "string", + "example": "https://jwt-idp.example.com" + } + } + }, + "trustedJwtGrantIssuer": { + "type": "object", + "properties": { + "created_at": { + "description": "The \"created_at\" indicates, when grant was created.", + "type": "string", + "format": "date-time" + }, + "expires_at": { + "description": "The \"expires_at\" indicates, when grant will expire, so we will reject assertion from \"issuer\" targeting \"subject\".", + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string", + "example": "9edc811f-4e28-453c-9b46-4de65f00217f" + }, + "issuer": { + "description": "The \"issuer\" identifies the principal that issued the JWT assertion (same as \"iss\" claim in JWT).", + "type": "string", + "example": "https://jwt-idp.example.com" + }, + "public_key": { + "$ref": "#/definitions/trustedJsonWebKey" + }, + "scope": { + "description": "The \"scope\" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749])", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "openid", + "offline" + ] + }, + "subject": { + "description": "The \"subject\" identifies the principal that is the subject of the JWT.", + "type": "string", + "example": "mike@example.com" + } + } + }, + "trustedJwtGrantIssuers": { + "type": "array", + "items": { + "$ref": "#/definitions/trustedJwtGrantIssuer" + } + }, "userinfoResponse": { "description": "The userinfo response", "type": "object", diff --git a/spec/config.json b/spec/config.json index 67ba2ce6f88..b46cfac1668 100644 --- a/spec/config.json +++ b/spec/config.json @@ -245,6 +245,32 @@ } } } + }, + "grantJwt": { + "type": "object", + "additionalProperties": false, + "description": "Authorization Grants using JWT configuration", + "properties": { + "jti_optional": { + "type": "boolean", + "description": "If false, JTI claim must be present in JWT assertion.", + "default": false + }, + "iat_optional": { + "type": "boolean", + "description": "If false, IAT claim must be present in JWT assertion.", + "default": false + }, + "max_ttl": { + "description": "Configures what the maximum age of a JWT assertion can be. Uses JWT's EXP claim and JWT IAT claim to calculate assertion age. Assertion, that exceeds max age will be denied. Useful as a safety measure and recommended to not be set to 720h max.", + "default": "720h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + } + } } }, "properties": { @@ -868,6 +894,15 @@ } } }, + "grant": { + "type": "object", + "additionalProperties": false, + "properties": { + "jwt": { + "$ref": "#/definitions/grantJwt" + } + } + }, "refresh_token_hook": { "type": "string", "description": "Sets the refresh token hook endpoint. If set it will be called during token refresh to receive updated token claims.", diff --git a/test/e2e/circle-ci.bash b/test/e2e/circle-ci.bash index 0ac23f849eb..ab595b81a59 100755 --- a/test/e2e/circle-ci.bash +++ b/test/e2e/circle-ci.bash @@ -53,90 +53,47 @@ export OAUTH2_EXPOSE_INTERNAL_ERRORS=1 export SERVE_PUBLIC_PORT=5000 export SERVE_ADMIN_PORT=5001 export LOG_LEAK_SENSITIVE_VALUES=true + export TEST_DATABASE_SQLITE="sqlite://$(mktemp -d -t ci-XXXXXXXXXX)/e2e.sqlite?_fk=true" +export TEST_DATABASE="$TEST_DATABASE_SQLITE" WATCH=no for i in "$@" do case $i in + memory) + # NOOP default value + ;; + postgres) + export TEST_DATABASE="$TEST_DATABASE_POSTGRESQL" + ;; + mysql) + export TEST_DATABASE="$TEST_DATABASE_MYSQL" + ;; + cockroach) + export TEST_DATABASE="$TEST_DATABASE_COCKROACHDB" + ;; + # Additional parameters --watch) - WATCH=yes - shift # past argument=value + WATCH=yes + ;; + --jwt) + export STRATEGIES_ACCESS_TOKEN=jwt + export OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public + export CYPRESS_jwt_enabled=true ;; *) - case "$i" in - memory) - ./hydra migrate sql --yes $TEST_DATABASE_SQLITE > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_SQLITE \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=false - ;; - - memory-jwt) - ./hydra migrate sql --yes $TEST_DATABASE_SQLITE > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_SQLITE \ - STRATEGIES_ACCESS_TOKEN=jwt \ - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=true - ;; - - postgres) - ./hydra migrate sql --yes $TEST_DATABASE_POSTGRESQL > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_POSTGRESQL \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=false - ;; - - postgres-jwt) - ./hydra migrate sql --yes $TEST_DATABASE_POSTGRESQL > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_POSTGRESQL \ - STRATEGIES_ACCESS_TOKEN=jwt \ - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=true - ;; - - mysql) - ./hydra migrate sql --yes $TEST_DATABASE_MYSQL > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_MYSQL \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=false - ;; - - mysql-jwt) - ./hydra migrate sql --yes $TEST_DATABASE_MYSQL > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_MYSQL \ - STRATEGIES_ACCESS_TOKEN=jwt \ - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=true - ;; - - cockroach) - ./hydra migrate sql --yes $TEST_DATABASE_COCKROACHDB > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_COCKROACHDB \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=false - ;; - - cockroach-jwt) - ./hydra migrate sql --yes $TEST_DATABASE_COCKROACHDB > ./hydra-migrate.e2e.log 2>&1 - DSN=$TEST_DATABASE_COCKROACHDB \ - STRATEGIES_ACCESS_TOKEN=jwt \ - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public \ - ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & - export CYPRESS_jwt_enabled=true - ;; - - *) - echo $"Usage: $0 {memory|postgres|mysql|cockroach|memory-jwt|postgres-jwt|mysql-jwt|cockroach-jwt} [--watch]" - exit 1 - esac + echo $"Invalid param $i" + echo $"Usage: $0 [memory|postgres|mysql|cockroach] [--watch][--jwt]" + exit 1 ;; esac done +./hydra migrate sql --yes $TEST_DATABASE > ./hydra-migrate.e2e.log 2>&1 + DSN=$TEST_DATABASE \ + ./hydra serve all --dangerous-force-http --sqa-opt-out > ./hydra.e2e.log 2>&1 & + npm run wait-on -- -l -t 300000 \ --interval 1000 -s 1 -d 1000 \ http-get://localhost:5000/health/ready http-get://localhost:5001/health/ready http-get://localhost:5002/ http-get://localhost:5003/oauth2/callback diff --git a/test/e2e/docker-compose.mysql.yml b/test/e2e/docker-compose.mysql.yml index d3b29c1ccc8..03396fc7e87 100644 --- a/test/e2e/docker-compose.mysql.yml +++ b/test/e2e/docker-compose.mysql.yml @@ -18,6 +18,7 @@ services: mysqld: image: mysql:5.7 + platform: linux/amd64 ports: - "3306:3306" environment: diff --git a/x/clean_sql.go b/x/clean_sql.go index 30e85d4765a..eb153106247 100644 --- a/x/clean_sql.go +++ b/x/clean_sql.go @@ -24,6 +24,7 @@ func CleanSQL(t *testing.T, db *sqlx.DB) { "hydra_oauth2_obfuscated_authentication_session", "hydra_oauth2_logout_request", "hydra_oauth2_jti_blacklist", + "hydra_oauth2_trusted_jwt_bearer_issuer", "hydra_jwk", "hydra_client", // Migrations @@ -56,6 +57,7 @@ func CleanSQLPop(t *testing.T, c *pop.Connection) { "hydra_oauth2_obfuscated_authentication_session", "hydra_oauth2_logout_request", "hydra_oauth2_jti_blacklist", + "hydra_oauth2_trusted_jwt_bearer_issuer", "hydra_jwk", "hydra_client", // Migrations diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 91d3f10fa16..dabb0a58ff3 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -28,6 +28,7 @@ import ( "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/pkce" + "github.com/ory/fosite/handler/rfc7523" ) type FositeStorer interface { @@ -35,6 +36,7 @@ type FositeStorer interface { oauth2.CoreStorage openid.OpenIDConnectRequestStorage pkce.PKCERequestStorage + rfc7523.RFC7523KeyStorage RevokeRefreshToken(ctx context.Context, requestID string) error