Skip to content

Add Support For JWT-Secured Authorization Requests in OIDC Connector#56990

Merged
rhammonds-teleport merged 15 commits intomasterfrom
rhammonds/signed-oauth
Aug 13, 2025
Merged

Add Support For JWT-Secured Authorization Requests in OIDC Connector#56990
rhammonds-teleport merged 15 commits intomasterfrom
rhammonds/signed-oauth

Conversation

@rhammonds-teleport
Copy link
Copy Markdown
Contributor

@rhammonds-teleport rhammonds-teleport commented Jul 21, 2025

Add new configuration parameter to OIDC Connector request_object_mode which forces the connector to use JWT-Secured Authorization Requests (RFC 9101) when invoking the IdP's authorization endpoint.

Changelog: Add support for JWT-Secured Authorization Requests to OIDC Connector

Comment thread api/types/oidc.go Outdated
// match for identifier-first login.
GetUserMatchers() []string
// GetEnableRequestObjects returns true if the connector should use JWT-Secured Authorization Requests when making auth requests
GetEnableRequestObjects() bool
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a bool, should we have a "mode" like we did for PKCEMode? This might make it easier for us to change defaults or support more than 2 options in the future. (For example, maybe the options are never, always, and auto-detect)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bad idea. Maybe we should also account for the possibility that we'll want to support "pass by reference" (request_uri) later on as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, In the future I could imagine supporting at least the following:

  • signed
  • signed_pushed (request_uri)
  • encrypted (also signed by definition)
  • encrypted_pushed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced EnableRequestObjects with RequestObjectMode and added associated constants. For now, we support none and signed, but could certainly add more (auto, encrypted, etc) as needed.

The default behavior is that of none (do not use request objects).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zmb3 you recommended following string PKCEMode as an example, but should we use a proto enum here, with the same custom marshaling logic used for RequireMFAType, SecondFactorType, etc? Or are we trying to move away from this pattern?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first thought was to use an enum as well. Though, I noticed that we have a lot of existing enums defined like so:

enum MySetting {
  OFF = 0;
  ON = 1;
}
// Will conflict with:
enum OtherSetting {
   OFF = 0;
   ON = 1;
}

Which causes conflicts in generated C++ code. Buf normally complains about this during linting but maybe we have it disabled. I would've had to get somewhat verbose with my enum values in order to achieve decent namespacing, so I opted to follow suit with PKCEMode.

Still, I'm open to changing it. Maybe there are good arguments for instead using enums that I'm missing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zmb3 you recommended following string PKCEMode as an example, but should we use a proto enum here, with the same custom marshaling logic used for RequireMFAType, SecondFactorType, etc? Or are we trying to move away from this pattern?

Sure, if we can get the marshalling correct then a proto enum is probably the most semantically correct.

We often fail to get the marshalling correct though, which results in hard-to-interpret configuration and audit events (they show up as integers instead of strings).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the custom marshaling route allow users to specify "signed" instead of 2 if they are managing the connector via the terraform provider? If not, then I would suggest proceeding with a string as that's a major pain point for any terraform users.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you're right Tim, we should move away from the enum+custom marshaling approach for the terraform provider and other external consumers of the protobuf spec.

Comment thread lib/jwt/jwt.go Outdated
return k.verify(token, expectedClaims)
}

// SignArbitrary creates a signed JWT with an arbitrary claims set
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SignArbitrary creates a signed JWT with an arbitrary claims set
// SignAny creates a signed JWT with an arbitrary claims set

Comment thread lib/jwt/jwt.go Outdated
Comment on lines +814 to +816
var kid string
var err error
if kid, err = KeyID(k.config.PublicKey); err != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var kid string
var err error
if kid, err = KeyID(k.config.PublicKey); err != nil {
kid, err := KeyID(k.config.PublicKey)
if err != nil {

This is probably personal preference, but I think the suggestion here better fits with the Teleport-style.

Comment thread api/types/oidc.go Outdated
Comment thread lib/jwt/jwt.go Outdated
Comment thread lib/jwt/jwt.go Outdated
Comment thread lib/jwt/jwt.go Outdated
Comment on lines +812 to +813
// SignArbitrary creates a signed JWT with an arbitrary claims set
func (k *Key) SignAny(claims map[string]any) (string, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is SignArbitrary, signature is SignAny

But, is there any minimal set of claims that we can make sure are set before signing a JWT? Does it not need at least an issuer and audience or something like that? If so do you think it would be appropriate to have a SignOAuthRequestObject and VerifyOAuthRequestObject method instead of SignAny/ValidateAny? That seems to be what we've done for other types of JWTs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issuer and audience aren't strictly required per RFC 9101, but you're right that there are a handful of required parameters for authorization requests that should always be included in the JAR object.

I think I can take inspiration from SignParamsJWTSVID which has a PrivateClaims field that can be used to include additional (optional?) claims.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed SignAny/ValidateAny methods and replaced them with more opinionated SignOIDCAuthRequest/VerifyOIDCAuthRequestToken alternatives. Hopefully these are less likely to be abused/misused. Open to discussing this further if I've missed the mark or if you have any other concerns.

I do feel a bit like I've pushed more OIDC/OAuth domain logic into this package than I would have liked, but I suppose it's just a bit of validation around required claims/parameters.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from RFC 9101

If signed, the Authorization Request Object SHOULD contain the Claims iss (issuer) and aud (audience) as members with their semantics being the same as defined in the JWT [RFC7519] specification. The value of aud should be the value of the authorization server (AS) issuer, as defined in RFC 8414 [RFC8414].

I guess it's only a SHOULD but since we are the ones issuing these we should probably go ahead and include those

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do feel a bit like I've pushed more OIDC/OAuth domain logic into this package than I would have liked

I'm aware of this guidance from the RFC, and I was on the fence about whether or not to include these claims. Typically I would say that OIDC client implementation itself is in a better position to make this decision rather than this package. Consider the off chance that we encounter a fussy IdP that dislikes these claims for whatever reason... but that's pretty unlikely. I'll go ahead and add them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more standard claims (including audience and issuer) in the companion PR.

Copy link
Copy Markdown
Contributor

@smallinsky smallinsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add new configuration parameter to OIDC Connector enable_request_objects which forces the connector to use JWT-Secured Authorization Requests (RFC 9101) when invoking the IdP's authorization endpoint.

What CA will be used to sign the JWT token on Teleport OIDC Relying Party side ?

Comment thread lib/jwt/jwt.go Outdated

var alg jose.SignatureAlgorithm
if alg, err = AlgorithmForPublicKey(k.config.PublicKey); err != nil {
return "", err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", err
return "", trace.Wrap(err)

Comment thread lib/jwt/jwt.go Outdated
}

return k.sign(claims, &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]interface{}{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ExtraHeaders: map[jose.HeaderKey]interface{}{
ExtraHeaders: map[jose.HeaderKey]any{

Comment thread lib/jwt/jwt.go Outdated
var kid string
var err error
if kid, err = KeyID(k.config.PublicKey); err != nil {
return "", err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", err
return "", trace.Wrap(err)

Comment thread lib/jwt/jwt.go Outdated
func (k *Key) validate(rawToken string) (map[string]any, error) {
tok, err := jwt.ParseSigned(rawToken)
if err != nil {
return nil, trace.Errorf("failed to parse jwt")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, trace.Errorf("failed to parse jwt")
return nil, trace.Errorf("failed to parse JWT")

Comment thread lib/jwt/jwt.go Outdated

return k.sign(claims, &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]interface{}{
"alg": alg,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does manual setting "alg" in the header is needed ? I thought that this is done by jose library internals.

@rhammonds-teleport rhammonds-teleport force-pushed the rhammonds/signed-oauth branch 2 times, most recently from 8eda678 to 35c690d Compare July 22, 2025 17:01
@rhammonds-teleport
Copy link
Copy Markdown
Contributor Author

Add new configuration parameter to OIDC Connector enable_request_objects which forces the connector to use JWT-Secured Authorization Requests (RFC 9101) when invoking the IdP's authorization endpoint.

What CA will be used to sign the JWT token on Teleport OIDC Relying Party side ?

The plan is to use the JWT CA for signing these request objects. We expect that the provider can be configured to retrieve public keys from our well-known JWKS endpoint.

@smallinsky
Copy link
Copy Markdown
Contributor

Add new configuration parameter to OIDC Connector enable_request_objects which forces the connector to use JWT-Secured Authorization Requests (RFC 9101) when invoking the IdP's authorization endpoint.

What CA will be used to sign the JWT token on Teleport OIDC Relying Party side ?

The plan is to use the JWT CA for signing these request objects. We expect that the provider can be configured to retrieve public keys from our well-known JWKS endpoint.

The types.JWTSigner right now is exclusively used only for Teleport Application access. The question is do we want to break OIDC SSO logic if the Application CA is rotated ?
For the public proxy where the OIDC SP auto fetches the JWKs from <proxy>/.well-known/jwks.json that might be a self healing issue. But for private cluster where JWKS export is used after Telepot APP CA rotation users will lost the access to teleport cluster.

@rhammonds-teleport
Copy link
Copy Markdown
Contributor Author

rhammonds-teleport commented Jul 23, 2025

Add new configuration parameter to OIDC Connector enable_request_objects which forces the connector to use JWT-Secured Authorization Requests (RFC 9101) when invoking the IdP's authorization endpoint.

What CA will be used to sign the JWT token on Teleport OIDC Relying Party side ?

The plan is to use the JWT CA for signing these request objects. We expect that the provider can be configured to retrieve public keys from our well-known JWKS endpoint.

The types.JWTSigner right now is exclusively used only for Teleport Application access. The question is do we want to break OIDC SSO logic if the Application CA is rotated ? For the public proxy where the OIDC SP auto fetches the JWKs from <proxy>/.well-known/jwks.json that might be a self healing issue. But for private cluster where JWKS export is used after Telepot APP CA rotation users will lost the access to teleport cluster.

Would you suggest adding a new CA specifically for this purpose then? It might be a little more apparent to administrators the ramifications of rotating their "OIDC" key.

edit: I just noticed that we have an oidc_idp CA already. Maybe that's more appropriate though it isn't advertised in our JWKS endpoint.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jul 24, 2025

Amplify deployment status

Branch Commit Job ID Status Preview Updated (UTC)
rhammonds/signed-oauth 1c36ad6 4 ✅SUCCEED rhammonds-signed-oauth 2025-08-08 18:18:09

Comment thread lib/jwt/jwt.go Outdated
}

// OIDCAuthRequestClaims defines the required parameters for a JWT-Secured Authorization Request object.
type OIDCAuthRequestClaims struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think we should embed jwt.Claims like other custom claims types do to include some standard JWT claims? I'm pretty sure we should be including at least an issuer, audience, and expiry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that my use case doesn't really match up with the intended purpose of this package, so I've removed my additions. There's at least one other package, boundkeypair, that handles JWT signing/serialization itself when it dealing with custom claims, so I'll follow this pattern in the OIDC implementation.

Comment thread lib/jwt/jwt.go Outdated
Comment on lines +898 to +912
// VerifyOIDCAuthRequestToken parses and validates a JWT that is to be interpreted as
// a JWT-Secured Authorization Request (JAR) object.
func (k *Key) VerifyOIDCAuthRequestToken(rawToken string) (OIDCAuthRequestClaims, error) {
claims, err := k.verifyOnly(rawToken)
if err != nil {
return OIDCAuthRequestClaims{}, trace.Wrap(err, "error verifying authorization request object signature")
}

request, err := oidcAuthRequestFromClaims(claims)
if err != nil {
return OIDCAuthRequestClaims{}, trace.Wrap(err, "error validating authorization request object parameters")
}

return request, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is only used in the test maybe we should define it only in the test file rather than exporting it. Otherwise I think it should be checking the standard JWT claims like expiry, issuedAt, subject, audience, etc.

Joerger and others added 9 commits August 8, 2025 13:57
…nnector to enable the use of JWT-Secured Authorization Requests

* Include key id and algorithm in JWT header when signing JARs
* Add a simple 'Validate' function to jwt.Key object allowing users to simply validate a signature and retrieve claims (registered and unregistered)
* Remove inconsistent go-jose/jwt/v5 dependency
* Use consistent map type for input/output of 'SignAny' and 'ValidateAny' methods
…ionated OIDC SignOIDCAuthRequestToken and VerifyOIDCAuthRequestToken alternatives
…string 'RequestObjectMode' which gives us more flexibility to support additional request object settings in the future.
…ast 'audience' and 'issuer' claims are set, and perform validation of claims during parsing/verification.

* Implement json.Marshaler and json.Unmarshaler interfaces on 'OIDCAuthRequestClaims' so that it can passed to go-jose utilities without pre/post transformation steps.`
@rhammonds-teleport rhammonds-teleport added this pull request to the merge queue Aug 13, 2025
Merged via the queue into master with commit 3df242d Aug 13, 2025
44 checks passed
@rhammonds-teleport rhammonds-teleport deleted the rhammonds/signed-oauth branch August 13, 2025 02:24
rhammonds-teleport added a commit that referenced this pull request Aug 19, 2025
…56990)

* POC make signer usable with jwt signing.

* Add SignOAuthRequest method.

* Update TODO; Add e ref.

* * Add new configuration parameter 'enable_request_objects' to oidc connector to enable the use of JWT-Secured Authorization Requests
* Include key id and algorithm in JWT header when signing JARs

* * Add setter for enabling/disabling JARs on an oidc connector
* Add a simple 'Validate' function to jwt.Key object allowing users to simply validate a signature and retrieve claims (registered and unregistered)

* Add comment on OIDCConnectorV3 setter

* * Rename 'SignOauthRequest' method to more generic 'SignAny'
* Remove inconsistent go-jose/jwt/v5 dependency
* Use consistent map type for input/output of 'SignAny' and 'ValidateAny' methods

* Remove generic JWT signing/verification methods and replace with opinionated OIDC SignOIDCAuthRequestToken and VerifyOIDCAuthRequestToken alternatives

* Replace 'EnableRequestObjects' setting on OIDCConnectorSpecV3 with a string 'RequestObjectMode' which gives us more flexibility to support additional request object settings in the future.

* Clean up tests, fix formatting, and pick (hopefully) better names for OIDC Auth Request related JWT methods.

* Periods at end of godoc comments

* Regenerate configuration docs/examples

* * Embed jwt.Claims within 'OIDCAuthRequestClaims', require that at least 'audience' and 'issuer' claims are set, and perform validation of claims during parsing/verification.
* Implement json.Marshaler and json.Unmarshaler interfaces on 'OIDCAuthRequestClaims' so that it can passed to go-jose utilities without pre/post transformation steps.`

* Remove OIDC Auth Request utilities from 'lib/jwt' package.

* Regenerate terraform docs

---------

Co-authored-by: joerger <bjoerger@goteleport.com>
rhammonds-teleport added a commit that referenced this pull request Aug 19, 2025
…56990)

* POC make signer usable with jwt signing.

* Add SignOAuthRequest method.

* Update TODO; Add e ref.

* * Add new configuration parameter 'enable_request_objects' to oidc connector to enable the use of JWT-Secured Authorization Requests
* Include key id and algorithm in JWT header when signing JARs

* * Add setter for enabling/disabling JARs on an oidc connector
* Add a simple 'Validate' function to jwt.Key object allowing users to simply validate a signature and retrieve claims (registered and unregistered)

* Add comment on OIDCConnectorV3 setter

* * Rename 'SignOauthRequest' method to more generic 'SignAny'
* Remove inconsistent go-jose/jwt/v5 dependency
* Use consistent map type for input/output of 'SignAny' and 'ValidateAny' methods

* Remove generic JWT signing/verification methods and replace with opinionated OIDC SignOIDCAuthRequestToken and VerifyOIDCAuthRequestToken alternatives

* Replace 'EnableRequestObjects' setting on OIDCConnectorSpecV3 with a string 'RequestObjectMode' which gives us more flexibility to support additional request object settings in the future.

* Clean up tests, fix formatting, and pick (hopefully) better names for OIDC Auth Request related JWT methods.

* Periods at end of godoc comments

* Regenerate configuration docs/examples

* * Embed jwt.Claims within 'OIDCAuthRequestClaims', require that at least 'audience' and 'issuer' claims are set, and perform validation of claims during parsing/verification.
* Implement json.Marshaler and json.Unmarshaler interfaces on 'OIDCAuthRequestClaims' so that it can passed to go-jose utilities without pre/post transformation steps.`

* Remove OIDC Auth Request utilities from 'lib/jwt' package.

* Regenerate terraform docs

---------

Co-authored-by: joerger <bjoerger@goteleport.com>
github-merge-queue bot pushed a commit that referenced this pull request Aug 19, 2025
…ector (#58063)

* Add Support For JWT-Secured Authorization Requests in OIDC Connector (#56990)

* POC make signer usable with jwt signing.

* Add SignOAuthRequest method.

* Update TODO; Add e ref.

* * Add new configuration parameter 'enable_request_objects' to oidc connector to enable the use of JWT-Secured Authorization Requests
* Include key id and algorithm in JWT header when signing JARs

* * Add setter for enabling/disabling JARs on an oidc connector
* Add a simple 'Validate' function to jwt.Key object allowing users to simply validate a signature and retrieve claims (registered and unregistered)

* Add comment on OIDCConnectorV3 setter

* * Rename 'SignOauthRequest' method to more generic 'SignAny'
* Remove inconsistent go-jose/jwt/v5 dependency
* Use consistent map type for input/output of 'SignAny' and 'ValidateAny' methods

* Remove generic JWT signing/verification methods and replace with opinionated OIDC SignOIDCAuthRequestToken and VerifyOIDCAuthRequestToken alternatives

* Replace 'EnableRequestObjects' setting on OIDCConnectorSpecV3 with a string 'RequestObjectMode' which gives us more flexibility to support additional request object settings in the future.

* Clean up tests, fix formatting, and pick (hopefully) better names for OIDC Auth Request related JWT methods.

* Periods at end of godoc comments

* Regenerate configuration docs/examples

* * Embed jwt.Claims within 'OIDCAuthRequestClaims', require that at least 'audience' and 'issuer' claims are set, and perform validation of claims during parsing/verification.
* Implement json.Marshaler and json.Unmarshaler interfaces on 'OIDCAuthRequestClaims' so that it can passed to go-jose utilities without pre/post transformation steps.`

* Remove OIDC Auth Request utilities from 'lib/jwt' package.

* Regenerate terraform docs

---------

Co-authored-by: joerger <bjoerger@goteleport.com>

* Allow OIDC Connector to Omit 'max_age' Parameter From Auth Requests (#57950)

* Check for 'TELEPORT_OIDC_OMIT_MFA_MAX_AGE' environment variable when adding MFA settings to OIDC connector config. This option allows users to configure teleport to omit the 'max_age' parameter from authorization requests to the IdP in rare cases where this behavior is necessary.

* More robust parsing of 'TELEPORT_OIDC_OMIT_MFA_MAX_AGE' value

* Need to explicitly set 'MaxAge' to nil when 'TELEPORT_OIDC_OMIT_MFA_MAX_AGE' is enabled, otherwise the base connector's 'MaxAge' setting will persist.

---------

Co-authored-by: joerger <bjoerger@goteleport.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants