From f69ae3bbe759c510a9ec2a99ecd6d7a97df2ea66 Mon Sep 17 00:00:00 2001 From: Marten Gajda Date: Sat, 11 Feb 2023 00:57:00 +0100 Subject: [PATCH] Provide method to encode/decode a OAuth2InteractiveGrant to/from String, closes #92 --- README.md | 13 ++++ build.gradle | 8 +- gradle.properties | 6 +- .../oauth2/client/OAuth2InteractiveGrant.java | 38 ++++++++-- .../client/grants/AuthorizationCodeGrant.java | 72 ++++++++++++++++++ .../oauth2/client/grants/ImplicitGrant.java | 72 ++++++++++++++++++ .../AuthorizationCodeTokenRequest.java | 2 +- .../ClientCredentialsTokenRequest.java | 6 +- .../http/requests/RefreshTokenRequest.java | 6 +- .../ResourceOwnerPasswordTokenRequest.java | 6 +- .../client/state/InteractiveGrantFactory.java | 72 ++++++++++++++++++ .../dmfs/oauth2/client/utils/GrantState.java | 26 +++++++ .../grants/AuthorizationCodeGrantTest.java | 76 +++++++++++++++++++ .../client/grants/ImplicitGrantTest.java | 67 ++++++++++++++++ .../AuthorizationCodeTokenRequestTest.java | 2 +- .../ClientCredentialsTokenRequestTest.java | 4 +- .../requests/RefreshTokenRequestTest.java | 4 +- ...ResourceOwnerPasswordTokenRequestTest.java | 4 +- .../tokens/ImplicitGrantAccessTokenTest.java | 4 +- .../client/tokens/JsonAccessTokenTest.java | 6 +- 20 files changed, 460 insertions(+), 34 deletions(-) create mode 100644 src/main/java/org/dmfs/oauth2/client/state/InteractiveGrantFactory.java create mode 100644 src/main/java/org/dmfs/oauth2/client/utils/GrantState.java create mode 100644 src/test/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrantTest.java create mode 100644 src/test/java/org/dmfs/oauth2/client/grants/ImplicitGrantTest.java diff --git a/README.md b/README.md index 11aa60f..924095e 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,19 @@ String authorization = String.format("Bearer %s", token.accessToken()); myConnection.setRequestProperty("Authorization", authorization); ``` +### Preserving state + +By design, an interactive grant (Authorization Code Grant and Implicit Grant) may require multiple round trips. Sometimes an application +needs to preserve the state between these round trips. For instance, an Android app may be suspended while the browser is in the foreground, and it may +have to preserve the grant state in order to receive the access token when it's resumed. + +This can be achieved by calling `encodedState()` on the `OAuth2InteractiveGrant` instance. This method returns a String that can later be used +to restore the grant state with + +```java +OAuth2InteractiveGrant grant = new InteractiveGrantFactory(oauth2Client).value(encodedState); +``` + ## Choice of HTTP client This library doesn't depend on any specific HTTP client implementation. Instead it builds upon [http-client-essentials-suite](https://github.com/dmfs/http-client-essentials-suite) to allow any 3rd party HTTP client to be used. diff --git a/build.gradle b/build.gradle index d48dfa9..623589e 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ if (project.hasProperty('SONATYPE_USERNAME') && project.hasProperty('SONATYPE_PA } dependencies { - api 'org.dmfs:jems:' + JEMS_VERSION + api 'org.dmfs:jems:1.44' api 'org.dmfs:rfc3986-uri:0.8.1' api 'org.dmfs:rfc5545-datetime:0.3' api 'org.dmfs:http-client-essentials:' + HTTP_CLIENT_ESSENTIALS_VERSION @@ -106,9 +106,13 @@ dependencies { implementation 'org.dmfs:http-client-types:' + HTTP_CLIENT_ESSENTIALS_VERSION implementation 'org.dmfs:http-client-basics:' + HTTP_CLIENT_ESSENTIALS_VERSION implementation 'org.dmfs:http-executor-decorators:' + HTTP_CLIENT_ESSENTIALS_VERSION + implementation 'org.dmfs:express-json:0.2.0' + implementation 'org.dmfs:jems2:' + JEMS2_VERSION implementation 'org.json:json:20220924' - testImplementation 'org.dmfs:jems-testing:' + JEMS_VERSION + testImplementation 'org.saynotobugs:confidence-core:0.8.0' + testImplementation 'org.saynotobugs:confidence-mockito4:0.8.0' + testImplementation 'org.dmfs:jems2-testing:' + JEMS2_VERSION testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:4.8.0' testImplementation 'org.hamcrest:hamcrest-library:2.2' diff --git a/gradle.properties b/gradle.properties index fb8afb6..ab0a8fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,3 @@ - - # # Copyright 2016 dmfs GmbH # @@ -15,10 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # - HTTP_CLIENT_ESSENTIALS_VERSION=0.20 -JEMS_VERSION=1.41 - +JEMS2_VERSION=2.14.0 POM_DEVELOPER_ID=dmfs POM_DEVELOPER_NAME=Marten Gajda POM_DEVELOPER_EMAIL=marten@dmfs.org diff --git a/src/main/java/org/dmfs/oauth2/client/OAuth2InteractiveGrant.java b/src/main/java/org/dmfs/oauth2/client/OAuth2InteractiveGrant.java index b564d85..f1001c3 100644 --- a/src/main/java/org/dmfs/oauth2/client/OAuth2InteractiveGrant.java +++ b/src/main/java/org/dmfs/oauth2/client/OAuth2InteractiveGrant.java @@ -20,6 +20,7 @@ import org.dmfs.httpessentials.exceptions.ProtocolError; import org.dmfs.httpessentials.exceptions.ProtocolException; import org.dmfs.rfc3986.Uri; +import org.json.JSONArray; import java.io.Serializable; import java.net.URI; @@ -39,7 +40,7 @@ public interface OAuth2InteractiveGrant extends OAuth2Grant * * @return A {@link URI}. */ - public URI authorizationUrl(); + URI authorizationUrl(); /** * Update the authentication flow with the redirect URI that was returned by the user agent. Unless this throws an Exception, the caller can assume that @@ -55,7 +56,7 @@ public interface OAuth2InteractiveGrant extends OAuth2Grant * @throws ProtocolException * If the redirectUri is invalid. */ - public OAuth2InteractiveGrant withRedirect(Uri redirectUri) throws ProtocolError, ProtocolException; + OAuth2InteractiveGrant withRedirect(Uri redirectUri) throws ProtocolError, ProtocolException; /** * Return a {@link Serializable} state object that can be used to retain the current authentication flow state whenever the original {@link @@ -68,13 +69,34 @@ public interface OAuth2InteractiveGrant extends OAuth2Grant * * @throws UnsupportedOperationException * if this grant type does not support exporting the current state. + * @deprecated in favour of {@link #encodedState()}. */ - public OAuth2GrantState state() throws UnsupportedOperationException; + @Deprecated + OAuth2GrantState state() throws UnsupportedOperationException; + + /** + * Returns a {@link String} that can be later used to retrieve an {@link OAuth2InteractiveGrant} with the same state. + *

+ * Note, the format of the String may be changed without further notice and may be incompatible with future versions of this library. + *

+ * Also note, the resulting String may contain secrets used during the interactive grant. Do not persist the result in its plain + * text form. Make sure to encrypt it with a secure cipher. + *

+ * Retrieve the original {@link OAuth2InteractiveGrant} like this: + *

{@code
+     * InteractiveGrant grant = new InteractiveGrantFactory(oauth2Client).value(encodedState);
+     * }
+     * 
+ */ + String encodedState(); /** * The interface of a simple {@link Serializable} object that represents the state of an interactive grant. + * + * @deprecated in favour of {@link OAuth2InteractiveGrant#encodedState()}. */ - public interface OAuth2GrantState extends Serializable + @Deprecated + interface OAuth2GrantState extends Serializable { /** * Creates an {@link OAuth2InteractiveGrant} from this state for the given client. @@ -86,6 +108,12 @@ public interface OAuth2GrantState extends Serializable * * @return An {@link OAuth2InteractiveGrant} that can be used to continue the authentication flow. */ - public OAuth2InteractiveGrant grant(OAuth2Client client); + OAuth2InteractiveGrant grant(OAuth2Client client); + } + + + interface OAuth2InteractiveGrantFactory + { + OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments); } } diff --git a/src/main/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrant.java b/src/main/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrant.java index 16fa2e5..c5a4b9d 100644 --- a/src/main/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrant.java +++ b/src/main/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrant.java @@ -16,6 +16,9 @@ package org.dmfs.oauth2.client.grants; +import net.iharder.Base64; + +import org.dmfs.express.json.elementary.JsonText; import org.dmfs.httpessentials.client.HttpRequestExecutor; import org.dmfs.httpessentials.exceptions.ProtocolError; import org.dmfs.httpessentials.exceptions.ProtocolException; @@ -23,15 +26,20 @@ import org.dmfs.oauth2.client.http.requests.AuthorizationCodeTokenRequest; import org.dmfs.oauth2.client.pkce.S256CodeChallenge; import org.dmfs.oauth2.client.scope.StringScope; +import org.dmfs.oauth2.client.utils.GrantState; import org.dmfs.rfc3986.Uri; import org.dmfs.rfc3986.encoding.Precoded; import org.dmfs.rfc3986.encoding.XWwwFormUrlEncoded; import org.dmfs.rfc3986.parameters.ParameterList; import org.dmfs.rfc3986.parameters.adapters.XwfueParameterList; import org.dmfs.rfc3986.parameters.parametersets.EmptyParameterList; +import org.dmfs.rfc3986.uris.LazyUri; +import org.dmfs.rfc3986.uris.Text; +import org.json.JSONArray; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; /** @@ -125,6 +133,7 @@ public OAuth2AccessToken accessToken(HttpRequestExecutor executor) throws IOExce } + @Deprecated @Override public OAuth2InteractiveGrant.OAuth2GrantState state() { @@ -132,6 +141,36 @@ public OAuth2InteractiveGrant.OAuth2GrantState state() } + @Override + public String encodedState() + { + return Base64.encodeBytes( + new JsonText(new GrantState(InitialAuthorizationCodeGrantFactory.class, + mScope.toString(), mState.toString(), mCodeVerifier.toString(), new XWwwFormUrlEncoded(mCustomParameters).toString())) + .value() + .getBytes(StandardCharsets.UTF_8)); + } + + + private final static class InitialAuthorizationCodeGrantFactory implements OAuth2InteractiveGrantFactory + { + + @Override + public OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments) + { + if (arguments.length() != 4) + { + throw new IllegalArgumentException("Can't restore grant from invalid state."); + } + return new AuthorizationCodeGrant(client, + new StringScope(arguments.getString(0)), + arguments.getString(1), + arguments.getString(2), + new XwfueParameterList(new Precoded(arguments.getString(3)))); + } + } + + /** * An {@link OAuth2InteractiveGrant} that represents the authorized state of an Authorization Code Grant. That means, the user has granted access and an * auth token was issued by the server. @@ -181,17 +220,49 @@ public OAuth2InteractiveGrant withRedirect(Uri redirectUri) } + @Deprecated @Override public OAuth2GrantState state() { return new AuthorizedAuthorizationCodeGrantState(mScope, mRedirectUri, mState, mCodeVerifier); } + + + @Override + public String encodedState() + { + return Base64.encodeBytes( + new JsonText(new GrantState(AuthenticatedAuthorizationCodeGrantFactory.class, + new Text(mRedirectUri).toString(), mScope.toString(), mState.toString(), mCodeVerifier.toString())) + .value() + .getBytes(StandardCharsets.UTF_8)); + } + + + private final static class AuthenticatedAuthorizationCodeGrantFactory implements OAuth2InteractiveGrantFactory + { + + @Override + public OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments) + { + if (arguments.length() != 4) + { + throw new IllegalArgumentException("Can't restore grant from invalid state."); + } + return new AuthorizedAuthorizationCodeGrant(client, + new LazyUri(new Precoded(arguments.getString(0))), + new StringScope(arguments.getString(1)), + arguments.getString(2), + arguments.getString(3)); + } + } } /** * An {@link OAuth2GrantState} that represents the state of an Authorization Code Grant that was not confirmed by the user so far. */ + @Deprecated private final static class InitialAuthorizationCodeGrantState implements OAuth2InteractiveGrant.OAuth2GrantState { @@ -229,6 +300,7 @@ public AuthorizationCodeGrant grant(OAuth2Client client) /** * An {@link OAuth2GrantState} that represents the state of an Authorization Code Grant that got user consent. */ + @Deprecated private final static class AuthorizedAuthorizationCodeGrantState implements OAuth2InteractiveGrant.OAuth2GrantState { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/dmfs/oauth2/client/grants/ImplicitGrant.java b/src/main/java/org/dmfs/oauth2/client/grants/ImplicitGrant.java index 94c742b..50a9a01 100644 --- a/src/main/java/org/dmfs/oauth2/client/grants/ImplicitGrant.java +++ b/src/main/java/org/dmfs/oauth2/client/grants/ImplicitGrant.java @@ -16,16 +16,25 @@ package org.dmfs.oauth2.client.grants; +import net.iharder.Base64; + +import org.dmfs.express.json.elementary.JsonText; import org.dmfs.httpessentials.client.HttpRequestExecutor; import org.dmfs.httpessentials.exceptions.ProtocolError; import org.dmfs.httpessentials.exceptions.ProtocolException; import org.dmfs.oauth2.client.*; import org.dmfs.oauth2.client.scope.StringScope; import org.dmfs.oauth2.client.tokens.ImplicitGrantAccessToken; +import org.dmfs.oauth2.client.utils.GrantState; import org.dmfs.rfc3986.Uri; +import org.dmfs.rfc3986.encoding.Precoded; +import org.dmfs.rfc3986.uris.LazyUri; +import org.dmfs.rfc3986.uris.Text; +import org.json.JSONArray; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; /** @@ -90,6 +99,7 @@ public OAuth2AccessToken accessToken(HttpRequestExecutor executor) throws IOExce } + @Deprecated @Override public OAuth2InteractiveGrant.OAuth2GrantState state() { @@ -97,6 +107,32 @@ public OAuth2InteractiveGrant.OAuth2GrantState state() } + @Override + public String encodedState() + { + return Base64.encodeBytes( + new JsonText(new GrantState(InitialImplicitGrantFactory.class, mScope.toString(), mState.toString())) + .value() + .getBytes(StandardCharsets.UTF_8)); + } + + + private final static class InitialImplicitGrantFactory implements OAuth2InteractiveGrantFactory + { + + @Override + public OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments) + { + if (arguments.length() != 2) + { + throw new IllegalArgumentException("Can't restore grant from invalid state."); + } + return new ImplicitGrant(client, new StringScope(arguments.getString(0)), arguments.getString(1)); + + } + } + + /** * An {@link OAuth2InteractiveGrant} that represents the authorized state of an Implicit Grant. That means, the user has granted access and an access token * was issued by the server. @@ -141,17 +177,50 @@ public OAuth2InteractiveGrant withRedirect(Uri redirectUri) } + @Deprecated @Override public OAuth2GrantState state() { return new AuthorizedImplicitGrantState(mRedirectUri, mScope, mState); } + + + @Override + public String encodedState() + { + return Base64.encodeBytes( + new JsonText(new GrantState(AuthenticatedImplicitGrantFactory.class, new Text(mRedirectUri).toString(), mScope.toString(), mState.toString())) + .value() + .getBytes(StandardCharsets.UTF_8)); + } + + + private final static class AuthenticatedImplicitGrantFactory implements OAuth2InteractiveGrantFactory + { + + @Override + public OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments) + { + if (arguments.length() != 3) + { + throw new IllegalArgumentException("Can't restore grant from invalid state."); + } + return new AuthorizedImplicitGrant(client, + new LazyUri(new Precoded(arguments.getString(0))), + new StringScope(arguments.getString(1)), + arguments.getString(2)); + } + } + } /** * An {@link OAuth2GrantState} that represents the state of an Implicit Grant that was not confirmed by the user so far. + * + * @deprecated */ + @Deprecated private final static class InitialImplicitGrantState implements OAuth2InteractiveGrant.OAuth2GrantState { @@ -179,7 +248,10 @@ public ImplicitGrant grant(OAuth2Client client) /** * An {@link OAuth2GrantState} that represents the state of an authorized Implicit Grant. + * + * @deprecated */ + @Deprecated private final static class AuthorizedImplicitGrantState implements OAuth2InteractiveGrant.OAuth2GrantState { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequest.java b/src/main/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequest.java index 19343be..8cb73b5 100644 --- a/src/main/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequest.java +++ b/src/main/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequest.java @@ -17,7 +17,7 @@ package org.dmfs.oauth2.client.http.requests; import org.dmfs.httpessentials.entities.XWwwFormUrlEncodedEntity; -import org.dmfs.jems.iterable.elementary.Seq; +import org.dmfs.jems2.iterable.Seq; import org.dmfs.oauth2.client.OAuth2AuthCodeAuthorization; import org.dmfs.oauth2.client.http.requests.parameters.AuthCodeParam; import org.dmfs.oauth2.client.http.requests.parameters.CodeVerifierParam; diff --git a/src/main/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequest.java b/src/main/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequest.java index efdb848..a3152a0 100644 --- a/src/main/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequest.java +++ b/src/main/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequest.java @@ -18,9 +18,9 @@ import org.dmfs.httpessentials.client.HttpRequest; import org.dmfs.httpessentials.entities.XWwwFormUrlEncodedEntity; -import org.dmfs.iterables.SingletonIterable; -import org.dmfs.iterables.elementary.PresentValues; -import org.dmfs.jems.iterable.composite.Joined; +import org.dmfs.jems2.iterable.Joined; +import org.dmfs.jems2.iterable.PresentValues; +import org.dmfs.jems2.iterable.SingletonIterable; import org.dmfs.oauth2.client.OAuth2Scope; import org.dmfs.oauth2.client.http.requests.parameters.GrantTypeParam; import org.dmfs.oauth2.client.http.requests.parameters.OptionalScopeParam; diff --git a/src/main/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequest.java b/src/main/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequest.java index 5d76d8b..e41df54 100644 --- a/src/main/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequest.java +++ b/src/main/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequest.java @@ -18,9 +18,9 @@ import org.dmfs.httpessentials.client.HttpRequest; import org.dmfs.httpessentials.entities.XWwwFormUrlEncodedEntity; -import org.dmfs.iterables.elementary.PresentValues; -import org.dmfs.jems.iterable.composite.Joined; -import org.dmfs.jems.iterable.elementary.Seq; +import org.dmfs.jems2.iterable.Joined; +import org.dmfs.jems2.iterable.PresentValues; +import org.dmfs.jems2.iterable.Seq; import org.dmfs.oauth2.client.OAuth2Scope; import org.dmfs.oauth2.client.http.requests.parameters.GrantTypeParam; import org.dmfs.oauth2.client.http.requests.parameters.OptionalScopeParam; diff --git a/src/main/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequest.java b/src/main/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequest.java index 533ecb7..9350251 100644 --- a/src/main/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequest.java +++ b/src/main/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequest.java @@ -18,9 +18,9 @@ import org.dmfs.httpessentials.client.HttpRequest; import org.dmfs.httpessentials.entities.XWwwFormUrlEncodedEntity; -import org.dmfs.iterables.elementary.PresentValues; -import org.dmfs.jems.iterable.composite.Joined; -import org.dmfs.jems.iterable.elementary.Seq; +import org.dmfs.jems2.iterable.Joined; +import org.dmfs.jems2.iterable.PresentValues; +import org.dmfs.jems2.iterable.Seq; import org.dmfs.oauth2.client.OAuth2Scope; import org.dmfs.oauth2.client.http.requests.parameters.GrantTypeParam; import org.dmfs.oauth2.client.http.requests.parameters.OptionalScopeParam; diff --git a/src/main/java/org/dmfs/oauth2/client/state/InteractiveGrantFactory.java b/src/main/java/org/dmfs/oauth2/client/state/InteractiveGrantFactory.java new file mode 100644 index 0000000..6cfe56c --- /dev/null +++ b/src/main/java/org/dmfs/oauth2/client/state/InteractiveGrantFactory.java @@ -0,0 +1,72 @@ +package org.dmfs.oauth2.client.state; + +import net.iharder.Base64; + +import org.dmfs.jems2.Function; +import org.dmfs.oauth2.client.OAuth2Client; +import org.dmfs.oauth2.client.OAuth2InteractiveGrant; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; + +import static org.dmfs.oauth2.client.OAuth2InteractiveGrant.OAuth2InteractiveGrantFactory; + + +/** + * A Function to return an {@link OAuth2InteractiveGrant} for a {@link String} that was previously retrieved from + * {@link OAuth2InteractiveGrant#encodedState()}. + */ +public final class InteractiveGrantFactory implements Function, java.util.function.Function +{ + private final OAuth2Client mOAuth2Client; + + + /** + * Creates an {@link OAuth2InteractiveGrantFactory} for the given {@link OAuth2Client}. + */ + public InteractiveGrantFactory(OAuth2Client oAuth2Client) + { + mOAuth2Client = oAuth2Client; + } + + + @Override + public OAuth2InteractiveGrant value(String state) + { + JSONObject object; + try + { + object = new JSONObject(new String(Base64.decode(state), StandardCharsets.UTF_8)); + } + catch (IOException e) + { + throw new RuntimeException("Unable to decode state", e); + } + JSONArray args = object.getJSONArray("args"); + String grantClass = object.getString("class"); + + try + { + Constructor constructor = (Constructor) Class.forName(grantClass) + .getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance().grant(mOAuth2Client, args); + } + catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException | + ClassCastException exception) + { + throw new IllegalArgumentException("Can't Instantiate OAuth2InteractiveGrantFactory implementation " + grantClass, exception); + } + } + + + @Override + public OAuth2InteractiveGrant apply(String encodedState) + { + return value(encodedState); + } +} diff --git a/src/main/java/org/dmfs/oauth2/client/utils/GrantState.java b/src/main/java/org/dmfs/oauth2/client/utils/GrantState.java new file mode 100644 index 0000000..d6f3547 --- /dev/null +++ b/src/main/java/org/dmfs/oauth2/client/utils/GrantState.java @@ -0,0 +1,26 @@ +package org.dmfs.oauth2.client.utils; + +import org.dmfs.express.json.elementary.Array; +import org.dmfs.express.json.elementary.DelegatingJsonValue; +import org.dmfs.express.json.elementary.Member; +import org.dmfs.express.json.elementary.Object; +import org.dmfs.jems2.iterable.Mapped; +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.oauth2.client.OAuth2InteractiveGrant; + + +public final class GrantState extends DelegatingJsonValue +{ + public GrantState(Class factoryClass, String... arguments) + { + this(factoryClass, new Seq<>(arguments)); + } + + + public GrantState(Class factoryClass, Iterable arguments) + { + super(new Object( + new Member("class", factoryClass.getName()), + new Member("args", new Array(new Mapped<>(org.dmfs.express.json.elementary.String::new, arguments))))); + } +} diff --git a/src/test/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrantTest.java b/src/test/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrantTest.java new file mode 100644 index 0000000..a07cfca --- /dev/null +++ b/src/test/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrantTest.java @@ -0,0 +1,76 @@ +package org.dmfs.oauth2.client.grants; + +import org.dmfs.httpessentials.client.HttpRequestExecutor; +import org.dmfs.oauth2.client.OAuth2AccessToken; +import org.dmfs.oauth2.client.OAuth2AuthorizationRequest; +import org.dmfs.oauth2.client.OAuth2Client; +import org.dmfs.oauth2.client.OAuth2InteractiveGrant; +import org.dmfs.oauth2.client.scope.BasicScope; +import org.dmfs.oauth2.client.state.InteractiveGrantFactory; +import org.dmfs.rfc3986.encoding.Precoded; +import org.dmfs.rfc3986.uris.LazyUri; +import org.junit.Test; + +import java.net.URI; + +import static org.dmfs.jems2.mockito.Mock.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + + +public class AuthorizationCodeGrantTest +{ + @Test + public void testInitialState() + { + OAuth2Client mockClient = mock(OAuth2Client.class, + with(OAuth2Client::randomChars, returning("123456789012345678901234567890")), + with(client -> client.authorizationUrl(any()), + answering(invocation -> ((OAuth2AuthorizationRequest) invocation.getArgument(0)).authorizationUri(URI.create("http://1234"))))); + + assertThat(new AuthorizationCodeGrant(mockClient, new BasicScope("token1", "token2")), + has("state", OAuth2InteractiveGrant::encodedState, + has("grant", new InteractiveGrantFactory(mockClient), + allOf( + instanceOf(AuthorizationCodeGrant.class), + has("authorization URI", OAuth2InteractiveGrant::authorizationUrl, + equalTo( + URI.create( + "http://1234?" + + "response_type=code&" + + "scope=token1+token2&" + + "state=123456789012345678901234567890&" + + "code_challenge_method=S256&" + + "code_challenge=9U5cj4EGSOdjjSXrftbSS35ZmdWI6Igm8qqDfS7lLs0")))))) + ); + } + + + @Test + public void testRedirectedState() + { + OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class, + with(Object::toString, returning("accessToken"))); + + HttpRequestExecutor executor = mock(HttpRequestExecutor.class); + + OAuth2Client mockClient = mock(OAuth2Client.class, + with(OAuth2Client::randomChars, returning("123456789012345678901234567890")), + with(OAuth2Client::redirectUri, returning(new LazyUri(new Precoded("http://xyz")))), + with(client -> client.authorizationUrl(any()), + answering(invocation -> ((OAuth2AuthorizationRequest) invocation.getArgument(0)).authorizationUri(URI.create("http://1234")))), + with(client -> client.accessToken(any(), eq(executor)), returning(accessToken))); + + assertThat(new AuthorizationCodeGrant(mockClient, new BasicScope("token1", "token2")), + has("redirected", grant -> grant.withRedirect( + new LazyUri(new Precoded("http://redirected?code=98765&state=123456789012345678901234567890"))), + has("state", OAuth2InteractiveGrant::encodedState, + has("grant", new InteractiveGrantFactory(mockClient), + has("authorization URI", grant -> grant.accessToken(executor), + equalTo(accessToken))))) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/oauth2/client/grants/ImplicitGrantTest.java b/src/test/java/org/dmfs/oauth2/client/grants/ImplicitGrantTest.java new file mode 100644 index 0000000..8b50e50 --- /dev/null +++ b/src/test/java/org/dmfs/oauth2/client/grants/ImplicitGrantTest.java @@ -0,0 +1,67 @@ +package org.dmfs.oauth2.client.grants; + +import org.dmfs.httpessentials.client.HttpRequestExecutor; +import org.dmfs.oauth2.client.OAuth2AccessToken; +import org.dmfs.oauth2.client.OAuth2AuthorizationRequest; +import org.dmfs.oauth2.client.OAuth2Client; +import org.dmfs.oauth2.client.OAuth2InteractiveGrant; +import org.dmfs.oauth2.client.scope.BasicScope; +import org.dmfs.oauth2.client.state.InteractiveGrantFactory; +import org.dmfs.rfc3986.encoding.Precoded; +import org.dmfs.rfc3986.uris.LazyUri; +import org.dmfs.rfc5545.Duration; +import org.junit.Test; + +import java.net.URI; + +import static org.dmfs.jems2.mockito.Mock.*; +import static org.mockito.ArgumentMatchers.any; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + + +public class ImplicitGrantTest +{ + @Test + public void testInitialState() + { + OAuth2Client mockClient = mock(OAuth2Client.class, + with(OAuth2Client::randomChars, returning("123456789012345678901234567890")), + with(client -> client.authorizationUrl(any()), + answering(invocation -> ((OAuth2AuthorizationRequest) invocation.getArgument(0)).authorizationUri(URI.create("http://1234"))))); + + assertThat(new ImplicitGrant(mockClient, new BasicScope("token1", "token2")), + has("state", OAuth2InteractiveGrant::encodedState, + has("grant", new InteractiveGrantFactory(mockClient), + allOf( + instanceOf(ImplicitGrant.class), + has("authorization URI", OAuth2InteractiveGrant::authorizationUrl, + equalTo( + URI.create( + "http://1234?response_type=token&scope=token1+token2&state=123456789012345678901234567890")))))) + ); + } + + + @Test + public void testRedirectedState() + { + HttpRequestExecutor executor = mock(HttpRequestExecutor.class); + + OAuth2Client mockClient = mock(OAuth2Client.class, + with(OAuth2Client::randomChars, returning("123456789012345678901234567890")), + with(OAuth2Client::redirectUri, returning(new LazyUri(new Precoded("http://xyz")))), + with(OAuth2Client::defaultTokenTtl, returning(new Duration(1, 0, 1000))), + with(client -> client.authorizationUrl(any()), + answering(invocation -> ((OAuth2AuthorizationRequest) invocation.getArgument(0)).authorizationUri(URI.create("http://1234"))))); + assertThat(new ImplicitGrant(mockClient, new BasicScope("token1", "token2")), + has("redirected", grant -> grant.withRedirect( + new LazyUri(new Precoded("http://redirected#access_token=1234567&state=123456789012345678901234567890"))), + has("state", OAuth2InteractiveGrant::encodedState, + has("grant", new InteractiveGrantFactory(mockClient), + has("authorization URI", grant -> grant.accessToken(executor), + has("token", OAuth2AccessToken::accessToken, equalTo("1234567")))))) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequestTest.java b/src/test/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequestTest.java index 49d7942..e08bcde 100644 --- a/src/test/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequestTest.java +++ b/src/test/java/org/dmfs/oauth2/client/http/requests/AuthorizationCodeTokenRequestTest.java @@ -20,7 +20,7 @@ import org.dmfs.httpessentials.client.HttpRequestEntity; import org.dmfs.httpessentials.types.MediaType; import org.dmfs.httpessentials.types.StringMediaType; -import org.dmfs.jems.hamcrest.matchers.optional.PresentMatcher; +import org.dmfs.jems2.hamcrest.matchers.optional.PresentMatcher; import org.dmfs.oauth2.client.OAuth2AuthCodeAuthorization; import org.dmfs.oauth2.client.OAuth2Scope; import org.dmfs.oauth2.client.scope.BasicScope; diff --git a/src/test/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequestTest.java b/src/test/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequestTest.java index f4b4782..2800204 100644 --- a/src/test/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequestTest.java +++ b/src/test/java/org/dmfs/oauth2/client/http/requests/ClientCredentialsTokenRequestTest.java @@ -30,8 +30,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import static org.dmfs.jems.hamcrest.matchers.LambdaMatcher.having; -import static org.dmfs.jems.hamcrest.matchers.optional.PresentMatcher.present; +import static org.dmfs.jems2.hamcrest.matchers.LambdaMatcher.having; +import static org.dmfs.jems2.hamcrest.matchers.optional.PresentMatcher.present; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertEquals; diff --git a/src/test/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequestTest.java b/src/test/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequestTest.java index d69410e..d7ded50 100644 --- a/src/test/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequestTest.java +++ b/src/test/java/org/dmfs/oauth2/client/http/requests/RefreshTokenRequestTest.java @@ -30,8 +30,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import static org.dmfs.jems.hamcrest.matchers.LambdaMatcher.having; -import static org.dmfs.jems.hamcrest.matchers.optional.PresentMatcher.present; +import static org.dmfs.jems2.hamcrest.matchers.LambdaMatcher.having; +import static org.dmfs.jems2.hamcrest.matchers.optional.PresentMatcher.present; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertEquals; diff --git a/src/test/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequestTest.java b/src/test/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequestTest.java index a75e173..f343994 100644 --- a/src/test/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequestTest.java +++ b/src/test/java/org/dmfs/oauth2/client/http/requests/ResourceOwnerPasswordTokenRequestTest.java @@ -30,8 +30,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import static org.dmfs.jems.hamcrest.matchers.LambdaMatcher.having; -import static org.dmfs.jems.hamcrest.matchers.optional.PresentMatcher.present; +import static org.dmfs.jems2.hamcrest.matchers.LambdaMatcher.having; +import static org.dmfs.jems2.hamcrest.matchers.optional.PresentMatcher.present; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertEquals; diff --git a/src/test/java/org/dmfs/oauth2/client/tokens/ImplicitGrantAccessTokenTest.java b/src/test/java/org/dmfs/oauth2/client/tokens/ImplicitGrantAccessTokenTest.java index 3267fb5..49d3de9 100644 --- a/src/test/java/org/dmfs/oauth2/client/tokens/ImplicitGrantAccessTokenTest.java +++ b/src/test/java/org/dmfs/oauth2/client/tokens/ImplicitGrantAccessTokenTest.java @@ -16,7 +16,7 @@ package org.dmfs.oauth2.client.tokens; -import org.dmfs.jems.hamcrest.matchers.optional.AbsentMatcher; +import org.dmfs.jems2.hamcrest.matchers.optional.AbsentMatcher; import org.dmfs.oauth2.client.scope.EmptyScope; import org.dmfs.rfc3986.encoding.Precoded; import org.dmfs.rfc3986.uris.LazyUri; @@ -24,7 +24,7 @@ import org.hamcrest.Matchers; import org.junit.Test; -import static org.dmfs.jems.hamcrest.matchers.optional.PresentMatcher.present; +import static org.dmfs.jems2.hamcrest.matchers.optional.PresentMatcher.present; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; diff --git a/src/test/java/org/dmfs/oauth2/client/tokens/JsonAccessTokenTest.java b/src/test/java/org/dmfs/oauth2/client/tokens/JsonAccessTokenTest.java index a18c076..66ca2ec 100644 --- a/src/test/java/org/dmfs/oauth2/client/tokens/JsonAccessTokenTest.java +++ b/src/test/java/org/dmfs/oauth2/client/tokens/JsonAccessTokenTest.java @@ -16,15 +16,15 @@ package org.dmfs.oauth2.client.tokens; -import org.dmfs.jems.hamcrest.matchers.optional.AbsentMatcher; +import org.dmfs.jems2.hamcrest.matchers.optional.AbsentMatcher; import org.dmfs.oauth2.client.OAuth2Scope; import org.dmfs.oauth2.client.scope.StringScope; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.Test; -import static org.dmfs.jems.hamcrest.matchers.optional.PresentMatcher.present; -import static org.dmfs.jems.mockito.doubles.TestDoubles.dummy; +import static org.dmfs.jems2.hamcrest.matchers.optional.PresentMatcher.present; +import static org.dmfs.jems2.mockito.doubles.TestDoubles.dummy; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat;