Skip to content

Commit

Permalink
feat: allow setting the access token type in client
Browse files Browse the repository at this point in the history
The access token type (`jwt` or `opaque`) can now be set in the
client configuration.  The value set here will overwrite the
global value for all flows concerning that client.
  • Loading branch information
hperl committed Feb 28, 2023
1 parent c3af131 commit af50266
Show file tree
Hide file tree
Showing 52 changed files with 2,281 additions and 1,574 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.bin/
.idea/
.vscode/
node_modules/
*.iml
*.exe
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ quicktest:
quicktest-hsm:
docker build --progress=plain -f .docker/Dockerfile-hsm --target test-hsm .

.PHONY: refresh
refresh:
UPDATE_SNAPSHOTS=true go test -failfast -short -tags sqlite,json1 ./...

authors: # updates the AUTHORS file
curl https://raw.githubusercontent.com/ory/ci/master/authors/authors.sh | env PRODUCT="Ory Hydra" bash

Expand Down
24 changes: 23 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import (
"strings"
"time"

"github.com/ory/hydra/v2/driver/config"
"github.com/ory/x/stringsx"

"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"

jose "gopkg.in/square/go-jose.v2" // Naming the dependency jose is important for go-swagger to work, see https://github.com/go-swagger/go-swagger/issues/1587
"gopkg.in/square/go-jose.v2" // Naming the dependency jose is important for go-swagger to work, see https://github.com/go-swagger/go-swagger/issues/1587

"github.com/ory/fosite"
"github.com/ory/hydra/v2/x"
Expand Down Expand Up @@ -291,6 +292,13 @@ type Client struct {
// RegistrationClientURI is the URL used to update, get, or delete the OAuth2 Client.
RegistrationClientURI string `json:"registration_client_uri,omitempty" db:"-"`

// OAuth 2.0 Access Token Strategy
//
// AccessTokenStrategy is the strategy used to generate access tokens.
// Valid options are `jwt` and `opaque`. `jwt` is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens
// Setting the stragegy here overrides the global setting in `strategies.access_token`.
AccessTokenStrategy string `json:"access_token_strategy,omitempty" db:"access_token_strategy" faker:"-"`

Lifespans
}

Expand Down Expand Up @@ -532,3 +540,17 @@ func (c *Client) GetEffectiveLifespan(gt fosite.GrantType, tt fosite.TokenType,
}
return *cl
}

func (c *Client) GetAccessTokenStrategy() config.AccessTokenStrategyType {
// We ignore the error here, because the empty string will default to
// the global access token strategy.
s, _ := config.ToAccessTokenStrategyType(c.AccessTokenStrategy)
return s
}

func AccessTokenStrategySource(client fosite.Client) config.AccessTokenStrategySource {
if source, ok := client.(config.AccessTokenStrategySource); ok {
return source
}
return nil
}
18 changes: 14 additions & 4 deletions client/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,14 @@ func (v *Validator) Validate(ctx context.Context, c *Client) error {
}

if c.SubjectType != "" {
if !stringslice.Has(v.r.Config().SubjectTypesSupported(ctx), c.SubjectType) {
return errorsx.WithStack(ErrInvalidClientMetadata.WithHintf("Subject type %s is not supported by server, only %v are allowed.", c.SubjectType, v.r.Config().SubjectTypesSupported(ctx)))
if !stringslice.Has(v.r.Config().SubjectTypesSupported(ctx, c), c.SubjectType) {
return errorsx.WithStack(ErrInvalidClientMetadata.WithHintf("Subject type %s is not supported by server, only %v are allowed.", c.SubjectType, v.r.Config().SubjectTypesSupported(ctx, c)))
}
} else {
if stringslice.Has(v.r.Config().SubjectTypesSupported(ctx), "public") {
if stringslice.Has(v.r.Config().SubjectTypesSupported(ctx, c), "public") {
c.SubjectType = "public"
} else {
c.SubjectType = v.r.Config().SubjectTypesSupported(ctx)[0]
c.SubjectType = v.r.Config().SubjectTypesSupported(ctx, c)[0]
}
}

Expand All @@ -173,6 +173,16 @@ func (v *Validator) Validate(ctx context.Context, c *Client) error {
}
}

if c.AccessTokenStrategy != "" {
s, err := config.ToAccessTokenStrategyType(c.AccessTokenStrategy)
if err != nil {
return errorsx.WithStack(ErrInvalidClientMetadata.
WithHintf("invalid access token strategy: %v", err))
}
// Canonicalize, just in case.
c.AccessTokenStrategy = string(s)
}

return nil
}

Expand Down
145 changes: 76 additions & 69 deletions cypress/integration/oauth2/authorize_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,98 @@

import { prng } from "../../helpers"

const accessTokenStrategies = ["opaque", "jwt"]

describe("The OAuth 2.0 Authorization Code Grant", function () {
const nc = () => ({
client_secret: prng(),
scope: "offline_access openid",
subject_type: "public",
token_endpoint_auth_method: "client_secret_basic",
redirect_uris: [`${Cypress.env("client_url")}/oauth2/callback`],
grant_types: ["authorization_code", "refresh_token"],
})
accessTokenStrategies.forEach((accessTokenStrategy) => {
describe("access_token_strategy=" + accessTokenStrategy, function () {
const nc = () => ({
client_secret: prng(),
scope: "offline_access openid",
subject_type: "public",
token_endpoint_auth_method: "client_secret_basic",
redirect_uris: [`${Cypress.env("client_url")}/oauth2/callback`],
grant_types: ["authorization_code", "refresh_token"],
access_token_strategy: accessTokenStrategy,
})

it("should return an Access, Refresh, and ID Token when scope offline_access and openid are granted", function () {
const client = nc()
cy.authCodeFlow(client, {
consent: { scope: ["offline_access", "openid"] },
})
it("should return an Access, Refresh, and ID Token when scope offline_access and openid are granted", function () {
const client = nc()
cy.authCodeFlow(client, {
consent: { scope: ["offline_access", "openid"] },
})

cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)
cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)

expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.not.be.empty
expect(refresh_token).to.not.be.empty
expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.not.be.empty
expect(refresh_token).to.not.be.empty
})
})
})

it("should return an Access and Refresh Token when scope offline_access is granted", function () {
const client = nc()
cy.authCodeFlow(client, { consent: { scope: ["offline_access"] } })
it("should return an Access and Refresh Token when scope offline_access is granted", function () {
const client = nc()
cy.authCodeFlow(client, { consent: { scope: ["offline_access"] } })

cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)
cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)

expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.be.undefined
expect(refresh_token).to.not.be.empty
expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.be.undefined
expect(refresh_token).to.not.be.empty
})
})
})

it("should return an Access and ID Token when scope offline_access is granted", function () {
const client = nc()
cy.authCodeFlow(client, { consent: { scope: ["openid"] } })
it("should return an Access and ID Token when scope offline_access is granted", function () {
const client = nc()
cy.authCodeFlow(client, { consent: { scope: ["openid"] } })

cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)
cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)

expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.not.be.empty
expect(refresh_token).to.be.undefined
expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.not.be.empty
expect(refresh_token).to.be.undefined
})
})
})

it("should return an Access Token when no scope is granted", function () {
const client = nc()
cy.authCodeFlow(client, { consent: { scope: [] } })
it("should return an Access Token when no scope is granted", function () {
const client = nc()
cy.authCodeFlow(client, { consent: { scope: [] } })

cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)
cy.get("body")
.invoke("text")
.then((content) => {
const {
result,
token: { access_token, id_token, refresh_token },
} = JSON.parse(content)

expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.be.undefined
expect(refresh_token).to.be.undefined
expect(result).to.equal("success")
expect(access_token).to.not.be.empty
expect(id_token).to.be.undefined
expect(refresh_token).to.be.undefined
})
})
})
})
})
Loading

0 comments on commit af50266

Please sign in to comment.