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: [#628] PAR implementation #660

Merged
merged 23 commits into from
Jul 19, 2022
Merged

Conversation

vivshankar
Copy link
Contributor

This PR implements RFC9126 - Pushed Authorization Request.

It introduces the following:

  1. New PushedAuthorizeEndpointHandlers
  2. Request and response writer handlers
  3. New PARStorage interface
  4. New functions for PAR lifecycle added to OAuth2Provider interface and corresponding implementation in Fosite
  5. New default handler for PAR called PushedAuthorizeHandler that complies with the PushedAuthorizeEndpointHandler interface

Related Issue or Design Document

#628 covers the details of the feature

Checklist

  • I have read the contributing guidelines
    and signed the CLA.
  • I have referenced an issue containing the design document if my change
    introduces a new feature.
  • I have read the security policy.
  • I confirm that this pull request does not address a security
    vulnerability. If this pull request addresses a security vulnerability, I
    confirm that I got green light (please contact
    [email protected]) from the maintainers to push
    the changes.
  • I have added tests that prove my fix is effective or that my feature
    works.
  • I have added necessary documentation within the code base (if
    appropriate).

Further comments

N/A

@aeneasr
Copy link
Member

aeneasr commented Apr 11, 2022

Just fyi, currently on vacation and not able to review this most likely in the next 2-3 weeks!

Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

Thank you very much, I have done a preliminary review of the PR. I still need to read the spec very carefully to see what we can and can't set as part of PAR.

What would be immensely helpful is if you could add code comments to the sections that specify things like "this value MUST be set" or similar. That way, we always know what values are set and why according to the spec.

I'm also not 100% sure how far we need to go with validation here? Do we validate e.g. redirect URLs as part of PAR, or do we validate them when we actually execute the PAR request? Or what about response types? I saw that the PAR handler checks e.g. scope and audience, but it's missing (unless I overlooked it) further validation steps.

Finally, it's not quite clear to me how an exemplary HTTP handler would look like - maybe you could add such a handler to ory/fosite-example?

Thank you so much!

defaultPARKeyLength = 32
)

var b64 = base64.URLEncoding.WithPadding(base64.NoPadding)
Copy link
Member

Choose a reason for hiding this comment

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

Is this up to us, or is it specified in a spec?

Copy link
Contributor

@james-d-elliott james-d-elliott Apr 15, 2022

Choose a reason for hiding this comment

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

I took a look at the spec and nothing specifically references the format of the reference value, it only references entropy requirements. Here is the specific section that discusses it (removed an irrelevant paragraph):

2.2. Successful Response:

If the verification is successful, the server MUST generate a request
URI and provide it in the response with a "201" HTTP status code.
The following parameters are included as top-level members in the
message body of the HTTP response using the "application/json" media
type as defined by [RFC8259].

request_uri
The request URI corresponding to the authorization request posted.
This URI is a single-use reference to the respective request data
in the subsequent authorization request. The way the
authorization process obtains the authorization request data is at
the discretion of the authorization server and is out of scope of
this specification. There is no need to make the authorization
request data available to other parties via this URI.

The format of the "request_uri" value is at the discretion of the
authorization server, but it MUST contain some part generated using a
cryptographically strong pseudorandom algorithm such that it is
computationally infeasible to predict or guess a valid value (see
Section 10.10 of [RFC6749] for specifics). The authorization server
MAY construct the "request_uri" value using the form
"urn:ietf:params:oauth:request_uri:<reference-value>" with
"<reference-value>" as the random part of the URI that references the
respective authorization request data.

10.10. Credentials-Guessing Attacks:

The authorization server MUST prevent attackers from guessing access
tokens, authorization codes, refresh tokens, resource owner
passwords, and client credentials.

The probability of an attacker guessing generated tokens (and other
credentials not intended for handling by end-users) MUST be less than
or equal to 2^(-128) and SHOULD be less than or equal to 2^(-160).

The authorization server MUST utilize other means to protect
credentials intended for end-user usage.

Thus the use of github.com/ory/fosite/token/hmac.RandomBytes(32) is correct and exceeds the entropy requirements at 2^(-256). Though I suspect you're asking about the encoding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's just a state type value. I don't really see a need to make this configurable as long as it meets the spec requirements, which it does.

Copy link
Member

Choose a reason for hiding this comment

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

I agree, what I wanted to know also was whether the vocabulary of B64 without padding (I think it's [a-zA-Z0-9+/]+) matches the one defined in the RFC

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The charset isn't dictated. It just requires the entropy for which I use the random number generator as @james-d-elliott indicated.

func (c *PushedAuthorizeHandler) HandlePushedAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.PushedAuthorizeResponder) error {
storage, ok := c.Storage.(fosite.PARStorage)
if !ok {
return errorsx.WithStack(fosite.ErrServerError.WithHint("Invalid storage type"))
Copy link
Member

Choose a reason for hiding this comment

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

Could we return a better error here - such as OAuth2 storage provider does not support Pushed Authorization Requests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we can do that. The expectation would be to never hit this state anyway. This would be a major misconfiguration, where the service is writing a HTTP handler calling into the PAR handlers (NewPushAuthorizationRequest etc.) without actually implementing the storage interface. This was really more of a safety net.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense! Good error messages can help tremendously when debugging, so even if this a coding error I think it just gives that extra 1% better developer experience :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return nil
}

func (c *PushedAuthorizeHandler) secureChecker() func(*url.URL) bool {
Copy link
Member

Choose a reason for hiding this comment

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

I would feel more comfortable with a signature of secureChecker(*url.URL) bool to avoid mistakes where the inner function is not properly being invoked because the caller messed up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok let me take a look. I think I copied this from somewhere else in Fosite.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

authorize_request_handler.go Show resolved Hide resolved
// Reject the request if the "request_uri" authorization request
// parameter is provided.
if requestURI, _ := claims["request_uri"].(string); isPARRequest && requestURI != "" {
return errorsx.WithStack(ErrInvalidRequestObject.WithHint("The request must not contain 'request_uri'."))
Copy link
Member

Choose a reason for hiding this comment

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

Clarify the error message a bit:

Pushed Authorization Requests can not contain the 'request_uri' parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I can make this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

request.RedirectURI = parRequest.GetRedirectURI()
request.ResponseTypes = parRequest.GetResponseTypes()
request.State = parRequest.GetState()
request.ResponseMode = parRequest.GetResponseMode()
Copy link
Member

Choose a reason for hiding this comment

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

Could you point to where these values are also set so that we ensure we did not forget setting some important ones? Could you please also point to the RFC section (and maybe add them as a comment) that state which parameters can be set as part of PAR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Link to RFC section

A client sends the parameters that comprise an authorization request directly to the PAR endpoint. A typical parameter set might include: client_id, response_type, redirect_uri, scope, state, code_challenge, and code_challenge_method as shown in the example below. However, the pushed authorization request can be composed of any of the parameters applicable for use at the authorization endpoint, including those defined in [[RFC6749](https://www.rfc-editor.org/rfc/rfc9126.html#RFC6749)] as well as all applicable extensions. The request_uri authorization request parameter is one exception, and it MUST NOT be provided.

These are set in the call to NewAuthorizeRequest from within the NewPushedAuthorizationRequest.


// validate the clients match
if clientID != request.GetClient().GetID() {
return false, errorsx.WithStack(ErrInvalidRequest.WithHint("The 'client_id' must match the pushed authorization request."))
Copy link
Member

Choose a reason for hiding this comment

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

The 'client_id' must match the one sent in the pushed authorization request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// No need to continue
return request, nil
} else if f.EnforcePushedAuthorization {
return request, errorsx.WithStack(ErrInvalidRequest.WithHint("Pushed authorization request is enforced."))
Copy link
Member

Choose a reason for hiding this comment

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

Pushed Authorization Requests are enforced but no such request was sent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

fosite.go Show resolved Hide resolved
handler/par/flow_pushed_authorize.go Show resolved Hide resolved
@aeneasr
Copy link
Member

aeneasr commented May 6, 2022

@vivshankar just wanted to check in and see if you still have appetite to get this merged :)

@vivshankar
Copy link
Contributor Author

vivshankar commented May 6, 2022

@aeneasr Yes. Just back from travel. I will look at this late next week. I responded to some of your comments. If you are happy with the ones where I have answered the question, perhaps you could mark those resolved. Please leave the ones that need changes from me unresolved, so I remember to fix those.

@aeneasr
Copy link
Member

aeneasr commented May 6, 2022

Thank you for the update @vivshankar - glad to have you back! I will look at the comments as soon as possible

@vivshankar
Copy link
Contributor Author

@aeneasr I have addressed the review comments (guess it didn't need to wait till next week :-) )

Here is an example of how a HTTP handler could be written. This is similar or same as the authorize HTTP handler func.

authorizeRequest, err := oauth2Provider.NewPushedAuthorizeRequest(ctx, r)
if err != nil {
  oauth2Provider.WritePushedAuthorizeError(rw, authorizeRequest, err)
  return
}

// Next we create a response
resp, err := oauth2Provider.NewPushedAuthorizeResponse(ctx, authorizeRequest, session)
if err != nil {
  oauth2Provider.WritePushedAuthorizeError(rw, authorizeRequest, err)
  return
}

oauth2Provider.WritePushedAuthorizeResponse(rw, authorizeRequest, resp)

@aeneasr aeneasr self-requested a review May 18, 2022 12:26
@aeneasr aeneasr self-assigned this May 18, 2022
Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

Thank you! Sorry for the long review times :(

Can you please add an integration tests for this feature? Since we're adding a completely new flow and endpoint it is required to ensure the code works as intended.

You can find an example here: https://github.com/ory/fosite/blob/master/integration/authorize_code_grant_test.go

@aeneasr
Copy link
Member

aeneasr commented May 24, 2022

And also add an example to: github.com/ory/fosite-example :)

@vivshankar
Copy link
Contributor Author

@aeneasr Not entirely clear what's failing with the linter. I have completed the changes as requested. I will add to fosite-example in a couple of days.

@aeneasr
Copy link
Member

aeneasr commented May 31, 2022

The URL got defunked, I'll fix it on master quickly

@vivshankar
Copy link
Contributor Author

@aeneasr Will you be able to review the latest changes and we can close this item? I am happy to contribute the new config provider for PAR as well.

@vivshankar
Copy link
Contributor Author

@aeneasr I have now made the changes based on the Configurator. Can you please review?

Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

Thank you! I have some comments left but overall this looks quite good!

pushed_authorize_request_handler.go Outdated Show resolved Hide resolved
pushed_authorize_request_handler.go Show resolved Hide resolved
clientID := r.Form.Get("client_id")

storage, ok := f.Store.(PARStorage)
if !ok {
Copy link
Member

Choose a reason for hiding this comment

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

We check on multiple occasions that the config or store implement PAR. We could use a single error var PARNotImplemented = ... and reuse that error instead of writing new errors for every check

pushed_authorize_request_handler.go Outdated Show resolved Hide resolved
integration/pushed_authorize_code_grant_test.go Outdated Show resolved Hide resolved
@mchristophe
Copy link

Hello everyone! Thanks for this PR and bringing this topic. This is exactly what I was needing. I am using Ory Hydra (which is great btw, thanks for the hard work!) and I was missing the PAR notion. Hope we will all be able to test it soon on our implementation of Hydra.

@mchristophe
Copy link

mchristophe commented Jul 14, 2022

I have another question regarding this implementation.
There are currently 2 topics regarding the parameters sent to the /authorize endpoint and some concerns regarding how it works today, especially within OpenBanking world where clients are asked to sign confidential transactions. There are ongoing discussions about how we should rethink the scope notion to be more granular, making them more dynamic.
While waiting for a proper answer from IETF, 2 topics regarding the subject:

  1. The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)
    RFC 9101
    : instead of sending the parameters, goal would be to provide a request object (JWT signed) sent to the AS, or sent as reference (to a place reachable by the AS)
  2. OAuth 2.0 Rich Authorization Requests: DRAFT new parameter authorization_details that is used to carry fine-grained authorization data in OAuth messages

The latest one would be a perfect fit for the PAR integration, while the 1st one would be a quick solution to manage custom parameters as secured as possible within financial sector.

My question is: could we join forces to proactively work on integrating those features from IETF ? I know the second one is in draft status, but we could already maybe analyse how it would fit into fosite ?

Thanks for your feedback :)

@vivshankar
Copy link
Contributor Author

@mchristophe I can take a crack because we seem to be on the same journey.

  • JAR: Fosite already supports signed request objects. So you are covered there. That format is also supported with the PAR implementation, so a client can send the signed request object as part of the PAR payload. I have my own extensions here that enforces the request object to contain all parameters other than client credentials, which is a requirement for FAPI 1.0 Advanced Final (if that is a concern for you).
  • Rich authorization request is something I have been paying close attention to and is a major advancement to capture fine-grained consents. In my personal opinion, the authorization_details make perfect sense for expressive consent statements, while scopes continue to be good enough for course grained consent statements. It is however still a draft, last I checked, and ory (understandably) waits for it to become a RFC. This is the same reason DPoP hasn't been implemented even though DPoP is the only available solution to enforce sender-constrained access tokens for SPAs.

@mchristophe
Copy link

@vivshankar : Question to finally clear my mind on that. When something is possible on Fosite (the JAR in this matter), does it mean that it is available on Hydra as well ? I know Hydra is an implementation of Fosite, so I guess the answer is yes. But the question popped up in my head while reading the discussion here.
Regarding your second point, I asked the authors of RAR draft why they were not using scopes instead of types within the json object definition (authorization_details). I have the feeling that doing so would assure backward compatibility w/ old oAuth implementation w/o too much effort. In the meantime, I guess I could already use that kind of new parameter within the request object and do my own check

@vivshankar
Copy link
Contributor Author

@mchristophe I expect so but since I don't work in ory or on Hydra, I will defer to @aeneasr to answer that.

@aeneasr
Copy link
Member

aeneasr commented Jul 19, 2022

Thank you @vivshankar - great answer!

@vivshankar : Question to finally clear my mind on that. When something is possible on Fosite (the JAR in this matter), does it mean that it is available on Hydra as well ? I know Hydra is an implementation of Fosite, so I guess the answer is yes. But the question popped up in my head while reading the discussion here.

Most of the times fixes land automatically in Ory Hydra. Here this is however not the case, as it requires implementation of additional endpoints and business logic. That in turn requires e2e tests, API specs, docs, ...

From maintainer side, this topic is far back in the list of priorities for Ory Hydra to support as we're focusing on v2 and releasing ory hydra to ory cloud

Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

Thank you for your perseverance! This was not an easy change to get merged and with other priorities coming in between I was not able to be as responsive as I wish I could have been. That's the price once you go professional, you don't have as much time any more to be on GitHub :/

A job well done - looking forward to see this in prod!

@aeneasr aeneasr merged commit 3de78db into ory:master Jul 19, 2022
@mchristophe
Copy link

Hello @aeneasr , thanks for your job! Indeed a very nice feature nicely handled. I did not understand your answer to my last question: does this mean PAR will be part of the next version of Hydra ? Sorry for asking this again, but I want to clear that out :). Thanks

@maricavor
Copy link

Hello, we are using Hydra version 2.2.0, but no PAR flow there, which version will support PAR flow?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants