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

Refactor OAuth 2.0 modules to easily implement extension grants #5476

Merged
merged 15 commits into from
Apr 9, 2024

Conversation

ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Feb 27, 2024

Motivation:

Currently, the access token grant flow is tightly coupled with AccessTokenRequest. As a result, we have to implement OAuth2XXXGrant, OAuth2XXXGrantBuilder, and XXXTokenRequest to implement a new grant flow.

An extension grant such as JWT is slightly different from the existing access token grant flow. The only difference is in body parameters. It is not practical to create three classes and use internal API for that.

To resolve the problem, I designed new APIs and refactored the existing OAuth2 module so that users could easily implement a new extension grant.

API design note:

  • OAuth2Request: Represents an OAuth 2.0 request that can be serialized to HttpRequest.
  • AccessTokenRequest: Provides body parameters and client authentication to obtain an access token.
    • This API will be used as an extension point to support new grants.
    • The implementations of AccessTokenRequest
      • ClientCredentialsAccessTokenRequest
      • ResourceOwnerPasswordAccessTokenRequest
      • JsonWebTokenAccessTokenRequest
  • ClientAuthentication: Provides client authentication for the OAuth 2.0 requests. OAuth2Request has ClientAuthentication as an optional value.
    • The interface is a drop-in replacement for ClientAuthorization which was a final class that does not allow customization.
    • Additionally, as per OAuth 2.0 spec, Client Authentication is a correct expession.
  • OAuth2ResponseHandler: Handles OAuth 2.0 responses.
    • This interface would be useful if users want to handle responses with a custom logic.
  • TokenOperationRequest: Provides body parameters for token revocation and token introspection.
  • OAuth2AuthorizationGrantBuilder: A unified builder to fluently create OAuth2AuthorizationGrant.
    • This will replace both OAuth2ResourceOwnerPasswordCredentialsGrantBuilder and OAuth2ClientCredentialsGrantBuilder.
    • No additional builder is required to support new grant types.

Example:

// Client credentials grant
AccessTokenRequest accessTokenRequest =
  AccessTokenRequest.ofClientCredentials("test_client", "client_secret");
OAuth2AuthorizationGrant grant = 
  OAuth2AuthorizationGrant
    .builder(authClient, "/token")
    .accessTokenRequest(accessTokenRequest)
    .build();

// Resource owner password grant
AccessTokenRequest accessTokenRequest =
  AccessTokenRequest.ofResourceOwnerPassword("user_name", "user_password");
OAuth2AuthorizationGrant grant = 
  OAuth2AuthorizationGrant
    .builder(authClient, "/token")
    .accessTokenRequest(accessTokenRequest)
    .build();

As you can see, the only difference is how you create AccessTokenRequest. The rest of the process will use the same API regardless of the grant type used.

Modifications:

  • Removed the following internal classes that are no longer used.
    • internal.client...AbstractAccessTokenRequest
    • internal.client...ClientCredentialsTokenRequest
    • internal.client...RefreshAccessTokenRequest
    • internal.client...ResourceOwnerPasswordCredentialsTokenRequest
    • internal.common...AbstractTokenOperationRequest
    • internal.common...TokenRevocationRequest
    • internal.common...TokenIntrospectionRequest
  • Added AccessTokenRequest and their implementations
    • The main difference from the old API is that the new AccessTokenRequest is an immutable class that only provides body parameters and client authentication.
    • AccessTokenRequest does not know how the request is processed. Implementations of OAuth2AuthorizationGrant and OAuth2ResponseHandler will take responsibility for it.
    • Implementations:
      • ClientCredentialsAccessTokenRequest
      • ResourceOwnerPasswordAccessTokenRequest
      • RefreshAccessTokenRequest
      • JsonWebTokenAccessTokenRequest (NEW)
  • Deprecated the legacy APIs
    • OAuth2ResourceOwnerPasswordCredentialsGrant
    • OAuth2ResourceOwnerPasswordCredentialsGrantBuilder
    • OAuth2ClientCredentialsGrant
    • OAuth2ClientCredentialsGrantBuilder
    • There are no breaking changes in the public API. The old APIs will delegate logic to the new API.
    • As they are unstable APIs, we may delete them after releasing some new minor versions.
  • Added ClientAuthentication that provides authentication information to Authorization header or body parameters.
    • Deprecated ClientAuthorization and internally delegated to ClientAuthentication.
    • Implementations:
      • ClientPasswordClientAuthentication
      • AnyAuthorizationSchemeClientAuthentication
      • JsonWebTokenClientAuthentication (NEW)
  • Added TokenOperationRequest to replace TokenRevocationRequest and TokenIntrospectionRequest.

Result:

Motivation:

Currently, access token grant flow is tightly coupled with
AccessTokenRequest. As a result, we have to implement
`OAuth2XXXGrant`, `OAuth2XXXGrantBuilder` and `XXXTokenRequest`
to implement a new grant flow.

An extension grant such as JWT is slightly different from the exisiting
access token grant flow. The only difference is in body parameters.
It is not pracitial to create three classes and use internal API for
that.

To resolve the problem, I designed new APIs and refactored the existing
OAuth2 module so that users can easily implement a new extention grant.

API design note:

- `OAuth2Request`: Represents an OAuth 2.0 request
  that can be serialized to `HttpRequest`.
- `AccessTokenRequest`: Provides body parameters and client
  authentication to obtain an access token.
  - This API will be used as an extension point to support new grants.
  - The implmentations of `AccessTokenRequest`
    - `ClientCredentialsAccessTokenRequest`
    - `ResourceOwnerPasswordAccessTokenRequest`
    - `JsonWebTokenAccessTokenRequest`
- `ClientAuthentication`: Provides a client authentication for the OAuth
   2.0 requests. `OAuth2Request` has `ClientAuthentication` as an
   optional value.
  - The interface is a drop-in replacement for `ClientAuthorization` which
    was a final class that does not allow customization.
  - Additionally, as per OAuth 2.0 spec, Client Authentication is a
    correct expession.
- `OAuth2ResponseHandler`: Handles OAuth 2.0 responses.
  - This interface would be useful if users want to handle responses
    with a custom logic.
- `TokenOperationRequest`: Provides body parameters for token
  revocation and token introspection.

Modifications:

- TBU
@ikhoon
Copy link
Contributor Author

ikhoon commented Feb 28, 2024

@max904-github Based on your work, I improved the API while maintaining the internal implementation. If available, please give me a review. 🙇‍♂️

@ikhoon ikhoon marked this pull request as ready for review February 28, 2024 02:42
Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

Looks much better now. 👍 👍 👍
Left minor comments and questions.

}
return bodyParams = OAuth2Request.super.bodyParams();
Copy link
Member

Choose a reason for hiding this comment

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

bodyParams does not contain clientAuthentication if bodyParams() is called before asHttpRequest is called.
What do you think about raising an exception in such a situation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored the code a bit to produce bodyParams correctly.

…ccessTokenRequest.java

Co-authored-by: minux <songmw725@gmail.com>
Copy link

codecov bot commented Mar 26, 2024

Codecov Report

Attention: Patch coverage is 60.98131% with 167 lines in your changes are missing coverage. Please review.

Project coverage is 74.06%. Comparing base (0b20455) to head (eaaf517).
Report is 61 commits behind head on main.

❗ Current head eaaf517 differs from pull request most recent head b60a9f7. Consider uploading reports for the commit b60a9f7 to get more accurate results

Files Patch % Lines
...ria/common/auth/oauth2/TokenRevocationBuilder.java 0.00% 22 Missing ⚠️
...client/auth/oauth2/AbstractAccessTokenRequest.java 56.41% 14 Missing and 3 partials ⚠️
...mmon/auth/oauth2/DefaultTokenOperationRequest.java 29.16% 16 Missing and 1 partial ⚠️
...mon/auth/oauth2/AbstractOAuth2ResponseHandler.java 38.46% 13 Missing and 3 partials ⚠️
...rp/armeria/common/auth/oauth2/TokenRevocation.java 0.00% 14 Missing ⚠️
...auth2/ResourceOwnerPasswordAccessTokenRequest.java 38.88% 11 Missing ⚠️
...h2/AnyAuthorizationSchemeClientAuthentication.java 35.29% 11 Missing ⚠️
...nt/auth/oauth2/JsonWebTokenAccessTokenRequest.java 33.33% 10 Missing ⚠️
.../client/auth/oauth2/RefreshAccessTokenRequest.java 33.33% 10 Missing ⚠️
...uth/oauth2/ClientPasswordClientAuthentication.java 64.28% 7 Missing and 3 partials ⚠️
... and 8 more
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #5476      +/-   ##
============================================
+ Coverage     74.02%   74.06%   +0.03%     
- Complexity    20781    21047     +266     
============================================
  Files          1801     1832      +31     
  Lines         76530    77493     +963     
  Branches       9749     9877     +128     
============================================
+ Hits          56654    57394     +740     
- Misses        15264    15421     +157     
- Partials       4612     4678      +66     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

LGTM once @trustin's comment is addressed.
Thanks a lot, @ikhoon! 👍 👍 👍

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Left some minor comments but overall direction looks good 👍 Thanks @ikhoon 🙇 👍 🙇 Please merge when ready 🙇

Comment on lines 98 to 101
* If null, this {@link ClientAuthentication} will be added as body parameters.
*/
@Nullable
String asHeaderValue();
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional) I found the logic in AbstractOAuth2Request slightly confusing.

final String headerValue = clientAuthentication.asHeaderValue();
if (headerValue == null) {
clientAuthentication.addAsBodyParams(formBuilder);
}

I wonder if it's more clear to create an API which looks similar to adding body parameters.

e.g.

void addHeaderValues(RequestHeadersBuilde)

By doing so, I think it is also easier to support other authentication methods easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I think my thoughts were too focused on compatibility with the existing ClientAuthorization.

Copy link
Member

@trustin trustin left a comment

Choose a reason for hiding this comment

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

🚀

@ikhoon ikhoon merged commit 333a16f into line:main Apr 9, 2024
16 checks passed
@ikhoon ikhoon deleted the refactor-oauth2 branch April 9, 2024 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants