Skip to content

Commit

Permalink
feat: support for urn:ietf:params:oauth:grant-type:jwt-bearer grant t…
Browse files Browse the repository at this point in the history
…ype RFC 7523 (#2384)

This change adds support for JSON Web Token (JWT) Profile for OAuth 2.0 Authorization Grants (RFC7523).
Users of Ory Hydra will be able to grant permission for OAuth 2.0 Client to act on behalf of some Resource Owner using JWT Bearer Assertions.

For more information about this feature, please head over to the documentation: https://www.ory.sh/hydra/docs/next/guides/oauth2-grant-type-jwt-bearer

Closes #2229

BREAKING CHANGES: Please notice that this change requires SQL migrations to be applied! As always, please make a backup before applying them!

Co-authored-by: aeneasr <3372410+aeneasr@users.noreply.github.com>
Co-authored-by: Jagoba Gascón <jagoba@arima.eu>
Co-authored-by: Gajewski Dmitriy <dmit8815@gmail.com>
  • Loading branch information
4 people authored Dec 26, 2021
1 parent 5bad542 commit 858f2cf
Show file tree
Hide file tree
Showing 83 changed files with 6,293 additions and 1,513 deletions.
9 changes: 5 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .docker/Dockerfile-alpine
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions .docker/Dockerfile-build
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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; \
Expand Down
2 changes: 1 addition & 1 deletion .docker/Dockerfile-scratch
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:3.14.3
FROM alpine:3.15

RUN apk add -U --no-cache ca-certificates

Expand Down
2 changes: 1 addition & 1 deletion .docker/Dockerfile-sqlite
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
14 changes: 5 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
29 changes: 5 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ that your company deserves a spot here, reach out to
<td>DataDetect</td>
<td align="center"><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/datadetect.svg" alt="Datadetect"></td>
<td><a href="https://unifiedglobalarchiving.com/data-detect/">unifiedglobalarchiving.com/data-detect/</a></td>
</tr>
</tr>
<tr>
<td>Adopter *</td>
<td>Sainsbury's</td>
Expand All @@ -190,7 +190,7 @@ that your company deserves a spot here, reach out to
<td>Reyah</td>
<td align="center"><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/reyah.svg" alt="Reyah"></td>
<td><a href="https://reyah.eu/">reyah.eu</a></td>
</tr>
</tr>
<tr>
<td>Adopter *</td>
<td>Zero</td>
Expand Down Expand Up @@ -264,26 +264,6 @@ TheCrealm.
<em>\* Uses one of Ory's major projects in production.</em>

<!--END ADOPTERS-->





















### OAuth2 and OpenID Connect: Open Standards!

Expand All @@ -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:

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
11 changes: 9 additions & 2 deletions cmd/cli/handler_janitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
ConsentRequestLifespan = "consent-request-lifespan"
OnlyTokens = "tokens"
OnlyRequests = "requests"
OnlyGrants = "grants"
ReadFromEnv = "read-from-env"
Config = "config"
)
Expand All @@ -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)
Expand Down Expand Up @@ -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...)...)
}

Expand All @@ -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
Expand Down
39 changes: 38 additions & 1 deletion cmd/cli/handler_janitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()))
})
}
}
15 changes: 10 additions & 5 deletions cmd/janitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func NewJanitorCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "janitor [<database-url>]",
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.
Expand Down Expand Up @@ -46,9 +46,13 @@ Janitor can be used in several ways.
janitor --requests <database-url>
or both
or
janitor --tokens --requests <database-url>
janitor --grants <database-url>
or any combination of them
janitor --tokens --requests --grants <database-url>
`,
RunE: cli.NewHandler().Janitor.RunE,
Args: cli.NewHandler().Janitor.Args,
Expand All @@ -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
Expand Down
50 changes: 46 additions & 4 deletions cypress/helpers/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
)
Loading

0 comments on commit 858f2cf

Please sign in to comment.