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

Add support for OAuth2 #189

Merged
merged 5 commits into from
May 22, 2023
Merged

Add support for OAuth2 #189

merged 5 commits into from
May 22, 2023

Conversation

jirijakes
Copy link
Contributor

@jirijakes jirijakes commented May 14, 2023

Draft of OAuth2 support, which should utlimately resolve #187 and hopefully will allow me to /claim #187.

Current status:

  • OAuth2 process (messages, polling, errors etc.)
  • Github support
  • Facebook support
  • Google support
  • Refreshing tokens
  • Storing of tokens to files
  • Pretty output
  • Figure out what to do with JS (HTTP client)
  • Finish🇫🇮 and polish🇵🇱 code (a few more types, remove ???, comments)
  • Figure out JSON decoders (is there no way to create decoder of a product from product of decoders?)
  • Documentation
  • Tests

Issues:

  • Works only on JVM with Java 11+
  • Requires additional dependency (ZIO JSON)

Showcase:

image

@CLAassistant
Copy link

CLAassistant commented May 14, 2023

CLA assistant check
All committers have signed the CLA.

@jdegoes
Copy link
Member

jdegoes commented May 14, 2023

@jirijakes Great to see this!

Indeed, we should support refresh tokens because it requires less interaction from the user, and leads to a better CLI experience.

For JS, the build can be split into JVM and JS source files, like ZIO itself and many other ZIO projects.

Let me know if you have any other questions!

@jirijakes
Copy link
Contributor Author

jirijakes commented May 14, 2023

we should support refresh tokens

Good!

Let me know if you have any other questions!

Thanks, John.

@jdegoes
Copy link
Member

jdegoes commented May 17, 2023

@jirijakes Do you know when you might get a chance to push this through to completion??

@jirijakes
Copy link
Contributor Author

jirijakes commented May 17, 2023

Do you know when you might get a chance to push this through to completion??

I am progressing very slowly during the week, will have more time Friday to Sunday. I believe on Sunday, it could be ready.

@jdegoes
Copy link
Member

jdegoes commented May 17, 2023

@jirijakes That's great, thanks for the update!

@jirijakes
Copy link
Contributor Author

Progress report

  • added storing of access token into encrypted file
    • password can by typed-in, provided by argument or provided by external command
    • file name can be provided by argument
  • added Google support
  • added Facebook support
  • added refreshing of access tokens

To make refreshing of token work seemlessly, I had to make access to access token effectful:

trait OAuth2Token {
  def refreshTokenNow: Task[Unit]
  def accessToken: Task[AccessToken]
}

With this, whenever accessToken is called, either the existing/active access token is returned immediately or new one is obtained by refreshing it. refreshTokenNow gives users opportunity to refresh access token on demand (e. g. when access token becomes invalid).

JavaScript

The biggest question I have now is what to do with JavaScript. Is the OAuth2 supposed to work with JS? I do have no experience with ScalaJS and when looking around ZIO projects, I could not find one that uses HTTP requests and file system. I found both should be possible with ScalaJS, however the lack of examples in ZIO ecosystem scares me a bit. Could I get an advice on this?

Connected to this is a problem I had with splitting code into JS/JVM. Since Options is a sealed trait, I cannot put case class OAuth2Options extends Options into platform-specific file. Even if OAuth2 does not support JS for the moment, I don't know how to properly make it only available on JVM (I could think of some ugly ways).

What's missing

Besides the JavaScript issue, a bit more documentation has to be added, error handling improved, remove a few ??? and some tests should be written.

Please, let me know if this approach is acceptable and which parts could be done better.

@jdegoes
Copy link
Member

jdegoes commented May 20, 2023

The biggest question I have now is what to do with JavaScript. Is the OAuth2 supposed to work with JS?

I would not worry about that. Just make sure it compiles under both JVM and JS. if the OAuth is not available in JS, that's fine. Anyone interested in that feature could add it themselves down the road.

Connected to this is a problem I had with splitting code into JS/JVM. Since Options is a sealed trait, I cannot put case class OAuth2Options extends Options into platform-specific file.

Put it into the common trait, just as yet another Options, alongside the existing ones; but push the implementation to a "support trait" which is implemented differently on JVM and JS. The JS implementation will just fail with an error indicating that OAuth is not yet supported on JS.

To make refreshing of token work seemlessly, I had to make access to access token effectful:

This is fine. 👍

added storing of access token into encrypted file

This sounds like it will lead to a negative user experience. Whatever we do here, it should have the usability of, e.g. the git command-line tool.

Many users store keys in ~/.ssh, and an access token is much more secure than that. So I would suggest storing the access tokens into a folder inside the user's home directory is enough.

Thank you for your work on this feature! I think you are nearly there.

@jirijakes
Copy link
Contributor Author

Ready for review.

Recent changes:

  • access token (together with refresh token, expiration time etc.) is stored in a plaintext file, location can be specified by --oauth2-file parameter. Default is current directory.
  • split into JS and JVM, JS not implemented
  • added documentation comments
  • tested all three providers, Google also with refresh token
  • error handling: the workflow is completely inside Task. It's mostly calling network and using filesystem plus some encoding/decoding of JSON. I could introduce some custom error types but at the end, I don't think it's worth since only an error message gets printed in case of problems. And that code is not supposed to be used as a library.

Not implemented:

  • tests; turns out there is not much to unit test without going full-blown ZIO (e. g. HTTP client) but I feel that would be quite overkill. Mostly we call some remote server and get response. If there are some suggestions, I will have a look.

@jirijakes jirijakes marked this pull request as ready for review May 21, 2023 09:26
@jdegoes
Copy link
Member

jdegoes commented May 21, 2023

Looks like there's a compiler crasher. Can you investigate and try to find a workaround? Usually working around Scala compiler's bugs is a matter of identifying the overall location and then experimenting until you find something that fixes it.

To switch SBT to the version that's crashing in the CI, do ++ <version> in the SBT console. Then you should be able to reproduce locally.

* @param expiresIn maximum period
* @return the last response received before terminating
*/
def pollingSchedule(
Copy link
Member

Choose a reason for hiding this comment

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

For things like this that we probably don't want to expose in the public API, be sure to mark private[cli].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hid the whole class, it's not supposed to be accessible from outside.

val expiresAt = time.plus(response.expiresIn).withNano(0).toLocalTime()

s"""| >>
| >> Application requests to perform OAuth2
Copy link
Member

Choose a reason for hiding this comment

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

Really good help docs!

scope: List[String],
auxiliaryOptions: Options[OAuth2AuxiliaryOptions]
) extends Options[OAuth2Token] {
override def helpDoc: HelpDoc = auxiliaryOptions.helpDoc
Copy link
Member

Choose a reason for hiding this comment

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

Rather than delegating everything to auxiliaryOptions, it may make sense to provide a bit of help here. But I suppose that depends on how it's rendered and if there is a good place to put help for a phantom options.

Copy link
Member

@jdegoes jdegoes left a comment

Choose a reason for hiding this comment

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

Relatively minor changes, overall looks amazing!

I think we should look at improving help doc a bit: at the top-level help page, we should discuss the OAuth2 support. That will require traversing the options in the CLIApp and seeieng if OAuth2Options is somewhere there, and if so, documenting the feature.

@jirijakes
Copy link
Contributor Author

Thanks for the review, John! I'll make the suggested changes.

Looks like there's a compiler crasher

It seems the cause is HttpClient, which is missing on Java 8 (it was added in Java 11).

So in order to make this work in Java 8, there are two options, as I can see: either use built-in, archaic HttpURLConnection, or a new external dependency.

Can you please advise, what would be best in line with the project?

I think we should look at improving help doc a bit: at the top-level help page, we should discuss the OAuth2 support.

Yes, this makes sense. I will see what I can do.

@jdegoes
Copy link
Member

jdegoes commented May 21, 2023

Let's bump JVM to 11

@jirijakes
Copy link
Contributor Author

Reflected John's comments.

I added help doc which appears when OAuth2 is used. See screenshot. I am not sure about wording and implementation (misusing p a bit but without me manually wrapping, it did not look good).

Let's bump JVM to 11

Shall I add it to this PR? Does it mean to remove Java 8 from ci.yml?

matrix:
java: [ '8', '11', '17' ]
scala: [ '2.12.17', '2.13.10', '3.2.2' ]
platform: [ 'JVM', 'JS', 'Native' ]

image

@jdegoes
Copy link
Member

jdegoes commented May 21, 2023

I added help doc which appears when OAuth2 is used. See screenshot. I am not sure about wording and implementation (misusing p a bit but without me manually wrapping, it did not look good).

Be sure that when --help is invoked, that no OAuth takes place, so that the user can see the help.

Shall I add it to this PR? Does it mean to remove Java 8 from ci.yml?

Yes, you modify the CI by making changes to the Github workflow. Instead of 8 we can just use 11.

@jdegoes
Copy link
Member

jdegoes commented May 21, 2023

@jirijakes

I think we should try to store the token in user home directory. Rather than the current directory. The reason is that the user may not have permission to modify the current directory, and also the user's current directory will keep changing all the time. You can get the home directory from Java properties:

System.getProperty("user.home")

@jirijakes
Copy link
Contributor Author

Be sure that when --help is invoked, that no OAuth takes place, so that the user can see the help.

Yup, verified.

Yes, you modify the CI by making changes to the Github workflow. Instead of 8 we can just use 11.

Done.

I think we should try to store the token in user home directory. Rather than the current directory.

Good idea. Done.

@jdegoes jdegoes merged commit 803022d into zio:master May 22, 2023
@jdegoes
Copy link
Member

jdegoes commented May 22, 2023

Thank you for your work on this!

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.

Integrate support for OAuth2 authentication & authorization
3 participants