Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Support for urn:ietf:params:oauth:grant-type:jwt-bearer grant type RFC 7523 #2384

Merged
merged 53 commits into from
Dec 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e550200
feat: add auth grant jwt support
Dec 17, 2020
ac8b3f4
fix: mysql migration for grant jwk and table naming
Dec 17, 2020
7474634
fix: check grant existence on attempt to delete it
Jan 19, 2021
b67fd7f
style: imports format
Jan 20, 2021
aabb2f0
docs: add swagger docs for grant API
Jan 21, 2021
c0463b8
fix: grant list was not filtered by optional issuer
Jan 21, 2021
b76c70b
test(grant): add tests for jwt-bearer grant
Jan 25, 2021
a5a33d8
test(grant): add public key scopes tests
Mar 4, 2021
8120145
fix(grant) delete assosiated grant public key on grant deletion
Mar 4, 2021
ebc1bd7
feat(fosite) use fosite v0.38.0
Mar 4, 2021
434bfa7
docs(readme): correct command to reset db in tests
Mar 4, 2021
81ba0ed
feat(grant): add flush handler
Mar 5, 2021
995db84
style: goimports
Mar 5, 2021
dc6354f
feat(config): changed config names for grant jwt
Mar 24, 2021
af66732
docs: add dummy page for oauth2 jwt grant type
aeneasr Apr 5, 2021
5948c87
docs(jwtbearer): Add docs for grant jwt bearer
May 24, 2021
de70e4e
doc(jwtbearer) change naming
May 25, 2021
71d84a8
chore(jwtbearer): regenerate sdk
May 25, 2021
9ee32ca
test(jwtbearer): add handler tests
May 28, 2021
1152a25
Merge remote-tracking branch 'origin/master' into auth-grant-type-jwt…
aeneasr Jul 13, 2021
84cba23
feat: code review
aeneasr Jul 13, 2021
bb5b3fd
feat: code review
aeneasr Jul 13, 2021
02a9a7e
feat: code review
aeneasr Jul 13, 2021
8f1f08d
feat: code review
aeneasr Jul 13, 2021
b349d06
feat: code review
aeneasr Jul 13, 2021
1fa9f77
feat: code review
aeneasr Jul 13, 2021
d4e2c2e
feat: code review
aeneasr Jul 13, 2021
e4bda4c
feat: code review
aeneasr Jul 13, 2021
b01cfab
feat: add end-to-end tests for the jwt bearer grant type (RFC 7523)
jagobagascon Nov 4, 2021
3ede4c0
feat: add grant validator tests
jagobagascon Nov 11, 2021
656c1c6
feat: use pipe instead of space to store jwt grant scopes in the DB
jagobagascon Nov 11, 2021
775a6e9
feat: add index to jwt bearer issuers expires_at column
jagobagascon Nov 11, 2021
91e24f9
refactor: get smallest time to save one DB filter when flushing grants
jagobagascon Nov 12, 2021
a882e6e
refactor: make DeleteGrant in a single transaction
jagobagascon Nov 12, 2021
b3fd42e
refactor: use a single transaction to in the CreateGrant function
jagobagascon Nov 12, 2021
8cb9f17
feat: remove flush expired grants endpoint and add it to the janitor CLI
jagobagascon Nov 15, 2021
1102b2f
fix: circleci
Nov 17, 2021
72862b1
feat: require client authentication when using the jwt bearer grant
jagobagascon Nov 22, 2021
756fab3
feat: remove unnecesary iat-optional and jti-optional flags
jagobagascon Dec 1, 2021
3d9b547
Merge pull request #5 from jagobagascon/auth-grant-type-jwt-bearer
drwatsno Dec 1, 2021
c51a51c
Merge remote-tracking branch 'origin/master' into auth-grant-type-jwt…
Dec 5, 2021
63e7660
fix: make contributors file
Dec 5, 2021
a0a48df
style: make format
Dec 5, 2021
259f23b
feat: programatically generate testing RSA key pairs
jagobagascon Dec 9, 2021
dd18daf
feat: replace Math.random with crypto.getRandomValues
jagobagascon Dec 9, 2021
9a59bd3
chore: format
Dec 14, 2021
8491478
Merge remote-tracking branch 'origin/master' into auth-grant-type-jwt…
aeneasr Dec 26, 2021
533f8e6
chore: code review
aeneasr Dec 26, 2021
8527fab
chore: code review
aeneasr Dec 26, 2021
a8f1a51
chore: format
aeneasr Dec 26, 2021
ac9f7ec
chore: code review
aeneasr Dec 26, 2021
5e161c8
chore: code review
aeneasr Dec 26, 2021
47996e0
u
aeneasr Dec 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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