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

proposal: x/oauth2: add Token.ExpiresIn struct field #61417

Open
andig opened this issue Jul 18, 2023 · 45 comments
Open

proposal: x/oauth2: add Token.ExpiresIn struct field #61417

andig opened this issue Jul 18, 2023 · 45 comments
Labels
Milestone

Comments

@andig
Copy link
Contributor

andig commented Jul 18, 2023

Migrated from golang/oauth2#484, refs #56402 (comment)

There are a number of OAuth2 token uses outside of the oauth2 library with the token structure being the common denominator. Unfortunately, unmarshaling JSON into an oauth2.Token does not populate it's Expiry field. Hence, the token structure needs be duplicated/embedded to provide this logic as it currently lives in oauth2/internal.

Proposed solution:

Allow unmarshaling an oauth2.Token from its usual json representation, i.e. containing the expires_in field.

An implementation choice might be to add a Token.UnmarshalJSON method though that might imply that (later) adding Token.MarshalJSON may not make sense given the nature of expires_in being relative to the current moment.

Consequence of not implementing:

Duplicated code like https://cs.github.com/?scopeName=All+repos&scope=&q=language%3Agolang+ExpiresIn+int#.

Alternatives:

@rsc
Copy link
Contributor

rsc commented Jul 26, 2023

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@rsc
Copy link
Contributor

rsc commented Aug 2, 2023

Is the specific proposal here to change Token.UnmarshalJSON to set the Expiry field based on expires_in?

@andig
Copy link
Contributor Author

andig commented Aug 2, 2023

Yes. Token.UnmarshalJSON is not exposed today. If it gets exposed it should set the Expiry field based on expires_in. If exposing/adding Token.UnmarshalJSON is not feasible the proposal would be to provide another method to the same result. Thank you!

@rsc
Copy link
Contributor

rsc commented Aug 9, 2023

I'm very confused, since Token.UnmarshalJSON does not exist and yet the top proposal above mentions it. What is the specific API change we are discussing? Or do you have a link to an implementation of the change?

@andig
Copy link
Contributor Author

andig commented Aug 9, 2023

I'm sorry for the confusion. The request here is to allow unmarshaling an oauth2.Token from its usual json representation, i.e. containing the expires_in field. Doing so by adding UnmarshalJson or exposing an existing internal function is irrelevant. I've updated the proposal.

@rsc
Copy link
Contributor

rsc commented Aug 16, 2023

What is the specific API that we should add?

@andig
Copy link
Contributor Author

andig commented Aug 17, 2023

@rsc I'm proposing to add the ability of unmarshaling tokens from JSON. I'm in no position to propose a specific api. As written before options I see are:

  1. adding Token.UnmarshalJSON
  2. add a new oauth2.TokenFromJSON method based on internal.tokenJSON

I feel the weekly round trips on this proposal do not really help to move it forward as apparently I cannot make the required contribution. Is there any chance one of the oauth2 owners joins here to improve the contribution?

@rsc
Copy link
Contributor

rsc commented Aug 30, 2023

@andig, I apologize for the unclear questions. When I ask what is the specific API, I mean what is the full godoc output for the API being added? For a function, what is the signature and the doc comment? For a struct field in an existing type, what is the name, type, and doc comment? And so on.

It sounds like Token.UnmarshalJSON is a method, so the question is how this gets filled out:

// UnmarshalJSON does ???
func (t *Token) UnmarshalJSON(???) (???)

and similarly for oauth2.TokenFromJSON.

@andig
Copy link
Contributor Author

andig commented Aug 30, 2023

@rsc I'm still slightly puzzled why we are discussing the signature instead of the best approach, but let me try!

Signature should be normal json.Unmarshaler, maintaining the same properties as oauth2 does today for compatibility, especially regarding raw values. It would be this if UnmarshalJSON is acceptable:

// UnmarshalJSON unmarshals an OAuth2 token from its JSON representation.
// The `expires_in` attribute is converted to a timestamp.
// The raw json attributes are preserved in `Raw`.
// If data contains `error` it will be converted into an error, containing `error_description` and `error_uri` if present.
func (t *Token) UnmarshalJSON(data []byte) error

Let's discuss TokenFromJSON if this is not acceptable to narrow the discussion?

@rsc
Copy link
Contributor

rsc commented Sep 6, 2023

Thanks for the clarification. It sounds like perhaps instead we should add an ExpiresIn int64 field to the Token so that people who want to unmarshal can do that. This would require them to consult ExpiresIn or to switch to Expiry by calling time.Now.Add themselves. It seems like a mistake to call time.Now during json.Unmarshal, since that will make json.Unmarshal produce non-repeatable results.

So what do people think of adding

// ExpiresIn is the OAuth2 wire format "expires_in" field,
// which specifies how many seconds later the token expires,
// relative to an unknown time base approximately around "now".
ExpiresIn int64`json:"expires_in,omitempty"`

If we add this, then json.Unmarshal will preserve the information, and applications can use it themselves.

@andig
Copy link
Contributor Author

andig commented Sep 6, 2023

The current Token has a special property of preserving the original JSON keys as part of Raw. It seems as if there are various deviations of token structure which makes this a useful feature. You would lose that but I don't know how important that is (and has not been requested here).

I like the idea- sweet and simple- but I'm wondering how many bugs the will create when one uses such an unmarshaled token and it is immediately expired due to Expiry not being populated. On the other hand, unmarshaling a persisted token would still restore Expiry which is nice. Should ExpiresIn be omitted from JSON export to keep the current structure? It's not useful without further context anyway.

I'd further suggest to add:

// It is application responsibility to populate `Expiry` from `ExpiresIn` when needed

@rsc
Copy link
Contributor

rsc commented Oct 4, 2023

Should ExpiresIn be omitted from JSON export

expires_in is the field that gets sent on the wire, so it seems wrong to omit it from JSON export.
It sounds like documentation is the best we can do about avoiding confusion.

Assuming we add ExpiresIn to resolve this proposal, have all remaining concerns been addressed?

@andig
Copy link
Contributor Author

andig commented Oct 4, 2023

Thank you, yes 👍🏻

@rsc
Copy link
Contributor

rsc commented Oct 11, 2023

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

@andig
Copy link
Contributor Author

andig commented Oct 12, 2023

@rsc rsc changed the title proposal: x/oauth2: Unmarshal expires_in outside of oauth2/internal proposal: x/oauth2: add Token.ExpiresIn struct field Oct 12, 2023
@hickford
Copy link

call time.Now during json.Unmarshal

oauth2.DeviceAuthResponse has the convenient property that its JSON encoding is consistent with OAuth RFC 8628 Device Authorization Response https://datatracker.ietf.org/doc/html/rfc8628#section-3.2

// DeviceAuthResponse describes a successful RFC 8628 Device Authorization Response 
type DeviceAuthResponse struct {
    // Expiry is when the device code and user code expire. When encoding or
    // decoding JSON, the following relation is used: Expiry = time.Now() + expires_in
    Expiry time.Time `json:"expires_in,omitempty"`

It would be neat if oauth2.Token and OAuth access token response JSON were consistent in the same way https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2

@andig
Copy link
Contributor Author

andig commented Oct 15, 2023

See @rsc's comment above:

It sounds like perhaps instead we should add an ExpiresIn int64 field to the Token so that people who want to unmarshal can do that. This would require them to consult ExpiresIn or to switch to Expiry by calling time.Now.Add themselves. It seems like a mistake to call time.Now during json.Unmarshal, since that will make json.Unmarshal produce non-repeatable results.

I agree that consistency in the codebase is a high value which might make it desirable to use the same approach for both tokens (and actually was what triggered me to ask #63543 before I discovered the Unmarshal implementation).

@hickford
Copy link

hickford commented Oct 15, 2023

So concretely?

type Token struct {
-    Expiry time.Time `json:"expiry,omitempty"`
+    Expiry time.Time
+    ExpiresIn int64 `json:"expires_in,omitempty"`
     ...
}

type DeviceAuthResponse struct {
-    Expiry time.Time `json:"expires_in,omitempty"`
+    Expiry time.Time
+    ExpiresIn int64 `json:"expires_in,omitempty"`
     ...
}

Then the JSON simply agrees with OAuth RFCs without custom MarshalJSON or UnmarshalJSON. That's appealing.

Functions Config.Exchange(...) and Config.DeviceAuth(...) must continue to populate Expiry based on time.Now + ExpiresIn.

@andig
Copy link
Contributor Author

andig commented Oct 17, 2023

@hickford Plus Expiry should get lower case json tag expiry. Should we continue in #63543 once this proposal has been accepted/implemented?

@hickford
Copy link

Why include expiry in JSON? If you are implementing an OAuth client, you receive JSON expires_in then store expiry time. If you are implementing an OAuth server, you respond with JSON expires_in and store token expiry time.

Any data structure with both expiry and expires_in can only be correct for an instant.

@rsc
Copy link
Contributor

rsc commented Oct 25, 2023

Thanks for pointing out DeviceAuthResponse. It does seem like we should take time to decide which of these two approaches should be used and then do that consistently. The fact that DeviceAuthResponse already does this makes it somewhat difficult to change, but not impossible.

@rsc
Copy link
Contributor

rsc commented Oct 26, 2023

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@andig
Copy link
Contributor Author

andig commented Oct 28, 2023

The fact that DeviceAuthResponse already does this makes it somewhat difficult to change, but not impossible.

DeviceAuthResponse already unmarshals expires_in. Token does not. Doing so was the proposal of this PR.

The discussion has then evolved to also include marshaling of expires_in in addition to Expiry since only the latter guarantees a stable validity date. That's what https://go-review.googlesource.com/c/oauth2/+/534835 implements.

DeviceAuthResponse does not marshal Expiry, or rather only does so as the relative expires_in, hence #63543 was opened.

I would suggest to:

@andig
Copy link
Contributor Author

andig commented Nov 2, 2023

Any objections to the updated proposal above?

@rsc would you want this proposal to include the resulting follow-up for DeviceAuth?

@andig
Copy link
Contributor Author

andig commented Nov 13, 2023

@rsc already tentatively accepted and no new comments. Could we proceed with this? Noticed that its no longer tracked in #33502.

@andig
Copy link
Contributor Author

andig commented Nov 21, 2023

@rsc 3 weeks in final comment period. No updates at all begins to feel a little frustrating. There are no open objections/comments and CL is done. Is there anything I could do to move this forward?

@andig
Copy link
Contributor Author

andig commented Dec 3, 2023

@rsc moved this from Likely Accept to Active in Proposals on Oct 27

Doesn't feel very active ;)

@andig
Copy link
Contributor Author

andig commented Dec 10, 2023

Sorry to say this, but more than 6 weeks have passed now. This feels absolutely unappreciated. More so since there's apparently no way to make it back into the proposal process. I've really liked working with and on Go, but not so this time.

/cc @rsc

@gophun
Copy link

gophun commented Jan 26, 2024

Maybe it was removed from the review meeting minutes template by accident. All other active proposals at least have a bullet point stating 'discussion ongoing' or something similar. This one appears to be the odd exception. Perhaps you could raise it on the golang-dev mailing list.

@andig
Copy link
Contributor Author

andig commented Jan 26, 2024

@seankhliao @rsc could you comment?

1 similar comment
@andig
Copy link
Contributor Author

andig commented Feb 7, 2024

@seankhliao @rsc could you comment?

@rsc
Copy link
Contributor

rsc commented Feb 8, 2024

Apologies for dropping this. We switched tracking systems in early November and for whatever reason we lost this one. I am going to add some extra checks to make sure we don't lose any in the future. This is back in the process now.

@rsc
Copy link
Contributor

rsc commented Feb 14, 2024

Given that DeviceAuthResponse already does the (arguably mistaken) time-based JSON marshal/unmarshal, it seems like doing the same for Token is at least consistently arguably mistaken. If we do the thing we were discussing before DeviceAuthResponse was pointed out, maybe it would be cleaner in isolation but then there would be two inconsistent and both weird behaviors in the package. Better to have one?

It sounds like we should go back to just using the existing fields and adding the time-based marshal/unmarshal to Token.

Maybe this can be cleaned up in an eventual v2, but for now it is what it is.

@andig
Copy link
Contributor Author

andig commented Feb 14, 2024

@rsc not sure I follow what you're proposing. Might be a language issue on my side.

Given that DeviceAuthResponse already does the (arguably mistaken) time-based JSON marshal/unmarshal

What do you mean by time-based and mistaken? Adding expires_in to time.Now()? That is indeed wrong, to be corrected by #63543.

If we do the thing we were discussing before DeviceAuthResponse was pointed out, maybe it would be cleaner in isolation but then there would be two inconsistent and both weird behaviors in the package.

The behaviour above is never cleaner, it is simply always wrong when Expiry is set. For Token that is not an issue since Expiry is already being (un)marshaled.

It sounds like we should go back to just using the existing fields and adding the time-based marshal/unmarshal to Token.

In the understanding above- yes. That's what this issue proposes: add additional(!) time-based marshal to Token. Since it is additional it is only used when Expiry is not set, such as when receiving token from wire.

Maybe this can be cleaned up in an eventual v2, but for now it is what it is.

I don't follow. What should be cleaned up after this proposal is accepted?

The longer the discussion goes, the less I understand what is being discussed here and what the objections are (are there any?).
To summarise: Token lacks the ability ot unmarshal expires_in. It's only ever used if Expiry is empty. Everything else about Token is fine. Lets add that capability. Separate PR: plus make sure DeviceAuth adds capability for Expiry as Token did for years.

@rsc
Copy link
Contributor

rsc commented Mar 1, 2024

Have all remaining concerns about this proposal been addressed?

The proposal is to add MarshalJSON and UnmarshalJSON methods to Token, similar to the ones in DeviceAuthResponse. Specifically:

func (Token) MarshalJSON() ([]byte, error)
func (*Token) UnmarshalJSON([]byte) error

and these would use the current time to marshal or unmarshal the Expiry field. This makes JSON operations time-dependent, but DeviceAuthResponse already did that.

@andig
Copy link
Contributor Author

andig commented Mar 3, 2024

@rsc that is NOT the proposal and

This makes JSON operations time-dependent, but DeviceAuthResponse already did that.

not the desired result :(

@andig
Copy link
Contributor Author

andig commented Mar 4, 2024

To repeat: request is to be able to unmarshal ExpiresIn. Approach is to add (Un)MarshalJSON and use ExpiresIn for setting Expiry when unmarshaling if and only when Expiry is empty. Doing so does not make JSON operations more time-dependent than today. Instead it adds a use case that is not possible today at all. Thank you!

@rsc
Copy link
Contributor

rsc commented Mar 8, 2024

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

The proposal is to add MarshalJSON and UnmarshalJSON methods to Token, similar to the ones in DeviceAuthResponse. Specifically:

func (Token) MarshalJSON() ([]byte, error)
func (*Token) UnmarshalJSON([]byte) error

and these would use the current time to marshal or unmarshal the Expiry field. This makes JSON operations time-dependent, but DeviceAuthResponse already did that.

@andig
Copy link
Contributor Author

andig commented Mar 8, 2024

This makes JSON operations time-dependent, but DeviceAuthResponse already did that.

This is exactly NOT what we want. Clarified once more in #61417 (comment).

@rsc I'm not sure why it seems as if you're pushing the proposal in that direction. It feels almost as if the remaining comments are not being read. Maybe mentioning the issues with DeviceAuthResponse was a mistake on my side as it apparently made the discussion more complex (then, DeviceAuthResponse was 2 weeks old...).

Since this would make it worse for oauth2.Token instead of fixing it (and then fixing DeviceAuthResponse) I'm closing this proposal. The process has been a frustrating experience :(

@andig andig closed this as completed Mar 8, 2024
@rsc
Copy link
Contributor

rsc commented Mar 13, 2024

@andig Apologies for the confusion and frustrating process. I am not 100% sure we are talking about the same thing. This issue started with golang/oauth2#484, where your top comment ended with:

It would be nice if oauth2.Token.UnmarshalJSON populated the Expiry field when expires_in is populated.

And I think that, after probably too long a discussion, that's where we ended up. The early comments here seemed to suggest that too. Are you saying that you don't think we should take that approach anymore?

Or is the objection about MarshalJSON also doing something with the field?

@rsc rsc reopened this Mar 13, 2024
@ianlancetaylor
Copy link
Contributor

We are in an awkward spot, but perhaps one way to move forward would be to add an ExpiresIn field to both DeviceAuthResponse and Token. When unmarshaling, if expires-in appears and expiry does not, we can both ExpiresIn and Expiry based on expires-in.

@andig
Copy link
Contributor Author

andig commented Mar 14, 2024

@ianlancetaylor that is exactly what was concluded above (and the CL does). That does not make Token time-dependent which is important. Seems that got lost in the sparse updates.
Imho ExpiresIn should not be marshaled by Token and neither by DeviceAuthResponse, but it seems the latter ship has sailed.

@rsc
Copy link
Contributor

rsc commented Mar 15, 2024

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@andig
Copy link
Contributor Author

andig commented Apr 5, 2024

@rsc proposal has once more escaped the proposal process and is not mentioned in the proposal groups's notes.

@rsc
Copy link
Contributor

rsc commented Apr 10, 2024

@andig not sure what you mean, it is listed near the bottom of #33502 (comment).

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

No branches or pull requests

6 participants