Skip to content

Commit

Permalink
Provide method to encode/decode a OAuth2InteractiveGrant to/from Stri…
Browse files Browse the repository at this point in the history
…ng, closes #92
  • Loading branch information
dmfs committed Feb 20, 2023
1 parent 9b74b5b commit f69ae3b
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 34 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
6 changes: 1 addition & 5 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


#
# Copyright 2016 dmfs GmbH
#
Expand All @@ -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=[email protected]
Expand Down
38 changes: 33 additions & 5 deletions src/main/java/org/dmfs/oauth2/client/OAuth2InteractiveGrant.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
* <p>
* Note, the format of the String may be changed without further notice and may be incompatible with future versions of this library.
* <p>
* 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.
* <p>
* Retrieve the original {@link OAuth2InteractiveGrant} like this:
* <pre>{@code
* InteractiveGrant grant = new InteractiveGrantFactory(oauth2Client).value(encodedState);
* }
* </pre>
*/
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.
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,30 @@

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.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;


/**
Expand Down Expand Up @@ -125,13 +133,44 @@ public OAuth2AccessToken accessToken(HttpRequestExecutor executor) throws IOExce
}


@Deprecated
@Override
public OAuth2InteractiveGrant.OAuth2GrantState state()
{
return new InitialAuthorizationCodeGrantState(mScope, mState, mCodeVerifier, new XWwwFormUrlEncoded(mCustomParameters));
}


@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.
Expand Down Expand Up @@ -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
{

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit f69ae3b

Please sign in to comment.